> built 2026-10-14 05:06 UTC from 5219317 (main) · an 0.1.239. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# an

AI-driven structured animation in Python. The user is the **director**; an AI agent (Claude Opus 4.7) is the **assistant orchestrator**; existing animation libraries (a custom 2D-cutout runtime, Manim, Remotion) are the **executors**.

```bash
pip install an
```

Renamed from `anima` (PyPI conflict). Repo: [https://github.com/thorwhalen/an](https://github.com/thorwhalen/an).

---

## What works today

```bash
an check                                                  # diagnose backend system deps
an init my-scene                                          # create a fresh project
# edit scene.md ...
an validate my-scene                                      # schema + semantic validation
an render my-scene                                        # → output/main.mp4 (offline defaults)
an render my-scene --tts elevenlabs --lipsync whisper     # real speech, word-aligned visemes
an render my-scene --parallel auto                        # per-shot threads (~N× speedup on N-shot scenes)
an render my-scene --step-hz 15                           # tweens "on twos" (15 pose updates/s at 30 fps); camera stays smooth
an preview my-scene                                       # live in-browser preview; reloads on edit
an iterate my-scene "make Maya laugh longer and warmer"   # free-text → IR patch → invalidate caches
an render my-scene                                        # re-renders only the affected shot

# Character authoring (Phase 11a)
an character new maya --seed maya-warm                    # generate a DiceBear-backed character
an character new bob --offline                            # offline-only: deterministic geometric fallback
an character mouths maya                                  # regenerate the 9-shape mouth set + its viseme@happy / viseme@sad variants
an character add-gaze maya                                # give an older character the sclera/pupil/lid eye stack (new ones have it)
an character validate maya                                # check parts, mouth set, pivots
an character silhouette maya --other bob                  # silhouette test (IoU score)
an character preview maya --open-browser                  # HTML viewer cycling all 9 visemes

# Synthetic impact clips with exact ground truth (for scoring sub-frame timing)
an impacts clip out/ --kind air --fps 30 --exposure 0.5 --jitter-sd 0.012
an impacts clip-set out/set --fps 24,30,60                # objects x kinds x fps x shutter, + index.json
```

The defaults run without any API keys: offline TTS produces silent audio of the right length, offline lip-sync deterministically generates viseme tracks. To get real audible speech, set `ELEVEN_API_KEY` and pass `--tts elevenlabs`. For word-aligned mouth shapes, `pip install faster-whisper` and pass `--lipsync whisper`. For free-text editing via `an iterate`, set `ANTHROPIC_API_KEY`.

---

## A 30-second tour

A project is a small directory:

```default
my-scene/
├── scene.md            # human Markdown — what you and the agent edit
├── ir/scene.json       # Pydantic-validated SSOT (auto-synced)
├── assets/             # characters, props, environments, voices, styles
├── artifacts/          # audio, viseme tracks, per-shot mp4s (content-hash cached)
├── output/             # final mp4s
└── .an/                # decisions log + agent memory
```

Authoring happens in `scene.md`:

```markdown
# Park Bench

```yaml meta
title: Park Bench
duration: 12
fps: 24
resolution: { width: 640, height: 360 }
```

## Shot s1 (cutout)

```yaml shot
duration: 6
camera: { move: push_in }
```

```yaml entities
- { kind: environment, id: park_bg, store: environments, ref: park }
- { kind: character,   id: charlie, store: characters,   ref: charlie-v1 }
- { kind: character,   id: maya,    store: characters,   ref: maya-v1 }
```

```dialogue
charlie [thinking]: Did you ever wonder why we always meet here?
maya [amused]: Because the pigeons trust us.
```
```

`an render` produces an mp4 with two visually-distinct characters (per-id palette: skin/clothing/hair), animated mouths over the dialogue lines, eye-blinks every ~4 seconds, brows, lids and mouth form driven by each line’s `[emotion]` (the face solver), a sky/grass park background, and a slow camera push-in.

---

## A 3-minute tour

`an` separates a scene into three layers:

1. **Narrative** — `scene.md`. Markdown with structured fenced blocks: `yaml meta`, `yaml shot`, `yaml entities`, `yaml actions`, `dialogue`. What you and the agent edit.
2. **Scene Graph** — `ir/scene.json`. Pydantic-validated, renderer-agnostic. The single source of truth. Diffable. Pipeline stages (audio synthesis, lip-sync) write into the JSON; `an sync` keeps it consistent with the Markdown using mtime-newer-wins.
3. **Render Code** — generated per-backend (cutout JSON for the JS runtime, Manim Python, Remotion TSX). Disposable; never edited by hand.

### Composition

Authoring is fluent in Python and flattens to a canonical timeline:

```python
from an import sequence, parallel, tween, delay, flatten

action = sequence(
    tween("charlie/torso", "rotation", to=10.0, duration=1.0),
    delay(0.5),
    tween("charlie/torso", "rotation", to=0.0, duration=1.0),
)
flat = flatten(action)
# [FlatAction(start=0.0, end=1.0, ...), FlatAction(start=1.5, end=2.5, ...)]
```

The same shape is also writable in markdown via a `yaml actions` block:

```yaml
- { kind: tween, target: charlie/torso, property: rotation, to: 10.0, duration: 1.0 }
- { kind: tween, target: charlie/torso, property: rotation, to: 0.0,  duration: 1.0, start: 1.5 }
```

### Persistence — the project mall

Everything long-lived (assets, artifacts, decisions, scene state) goes through a `dol`-backed `MutableMapping` mall:

```python
from an import build_project_mall

mall = build_project_mall("my-scene", ensure=True)
mall["voices"]["maya-warm"] = {"provider": "elevenlabs", "voice_id": "..."}
mall["scenes"]["main"]  # returns a SceneIR
```

### End-to-end orchestration

```python
from an.orchestrate import orchestrate

report = orchestrate("my-scene")
# report.success: bool
# report.output_path: Path
# report.validation:  ValidationReport
# report.verifications: list[VerificationReport] from each verifier
```

The default verifier chain runs `LayoutLintVerifier` (pre + post) and `MediaQualityVerifier` (post). Pass your own list to swap in `VisionLMVerifier` (Claude vision QA) or `HumanInTheLoopVerifier` (interactive approval).

### Free-text iteration (the spec’s signature loop)

```python
from an.orchestrate import iterate

result = iterate("my-scene", "Make Maya's response a bit longer and more affectionate")
# result.summary:           "Extended Maya's reply..."
# result.patches:            [Patch(op='set', path='timeline/1/dialogue/0/text', value=...), ...]
# result.affected_shots:     ['s2']
# result.success / .error / .new_scene / .validation
```

The patches are validated against the schema and persisted; affected shots’ cached mp4s are invalidated so the next `an render` regenerates only those.

---

## Architecture

| Subsystem             | Implementation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Renderer Protocol** | `an.adapters.Renderer` — Cutout (real), Manim (real if installed), Remotion (skeleton), Whiteboard (stub)                                                                                                                                                                                                                                                                                                                                                                                                                           |
| **Cutout backend**    | `an.adapters.cutout.compile_shot` → `CutoutSceneJSON` → PixiJS v7 in headless Chromium → ffmpeg mux                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| **Character rig**     | Ellipse head + per-id palette (skin/clothing/hair) + eyebrows and eyelids driven by the compile-time face solver (`expression` actions, ten presets, the dialogue `[emotion]` sugar; `viseme@<form>` mouth-set variants) + eyes as a sclera/pupil/lid stack (the pupils follow `gaze_x`/`gaze_y` and make seeded ambient saccades; blinks compiled as channels — an eyelid swap where the rig has closed-eye art, a squash otherwise; `an character add-gaze` gives an older rig the stack) + bezier-curved mouth (9 viseme shapes) |
| **TTS Protocol**      | `OfflineTTS` (silent placeholder), `ElevenLabsTTS` (real, needs `ELEVEN_API_KEY`)                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| **Lip-sync Protocol** | `OfflineLipSync` (char-distribution), `WhisperLipSync` (word-aligned via faster-whisper), `RhubarbLipSync` (phoneme-aligned), `WordTimingsLipSync` (driven by an injected `WordTimingProvider` — skip transcription entirely when the caller already has authoritative word timings)                                                                                                                                                                                                                                                |
| **Verifier Protocol** | `LayoutLintVerifier`, `MediaQualityVerifier`, `VisionLMVerifier` (Claude vision), `HumanInTheLoopVerifier`                                                                                                                                                                                                                                                                                                                                                                                                                          |
| **Persistence**       | dol-backed `MutableMapping`s organized into `build_project_mall(...)`                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **CLI**               | `typer` dispatch over `an.tools._dispatch_funcs` (init / validate / sync / check / render / iterate / bench) plus the `an character ...` and `an impacts ...` namespaces                                                                                                                                                                                                                                                                                                                                                            |
| **Iterate loop**      | `an.iterate` — Anthropic Opus 4.7 + adaptive thinking + structured JSON patches + path-based mutation + cache invalidation                                                                                                                                                                                                                                                                                                                                                                                                          |

For a deeper as-built reference (module-by-module map, control flows, key invariants, content-hash caching strategy), see [`misc/docs/architecture_as_built.md`](). The research reports next to it cover the design space the system was built against — they are not current state, and `wave1_verification.md` is (verified fact, with the URLs each licence was read at).

### Injecting word timings (no whisper redundancy)

When the caller already has authoritative word-level alignment data
(e.g. from a separate lyric-alignment pipeline), `an` can skip its
own transcription pass entirely:

```python
from an.audio import StaticWordTimings, WordTimingsLipSync
from an.orchestrate import orchestrate

timings = [("hello", 0.5, 1.0), ("world", 1.2, 1.8), ...]
lipsync = WordTimingsLipSync(StaticWordTimings(timings, label="my-aligner"))

orchestrate("my-scene", lipsync=lipsync)
```

`orchestrate(..., tts=, lipsync=, parallel=)` accepts either provider
name strings or instances. Plug in any `WordTimingProvider`
(structural protocol with `name: str` and `words_for(audio, transcript=)` returning `(text, start, end)` tuples). `muvid` uses
this hook to feed `lacing` alignment-store timings straight into the
cutout pipeline.

### Synthetic impact clips (`cutan.impacts`)

Structured animations of a stick or a ball striking a surface — or striking *the air*, a stroke that turns with nothing to hit — on a known tempo grid, filmed by a camera whose frame rate, shutter and capture jitter you choose. Each clip ships a sidecar that keeps three times apart, which is what makes it useful for scoring estimators that try to recover an impact *between* two frames: the **intended** grid time, the **executed** impact time (grid plus humanisation, in continuous seconds — never snapped to a frame), and **what the frames show** (each frame’s exposure interval and sample instants, per-frame keypoints, and which frames bracket each impact).

```python
from cutan.impacts import ImpactClipSpec, write_impact_clip, write_impact_set

spec = ImpactClipSpec(
    object="stick",
    kind="air",  # or "ball" / "surface"
    tempo=[(0, 90), (16, 120)],
    beats=16,  # an accelerando; or a single bpm
    pattern=(1, 0.5, 0.8, 0.5),  # per-step stroke height, 0 = rest
    jitter_sd=0.012,
    jitter_rho=0.3,  # human timing, seconds (default 8 ms; 0 = metronome)
    fps=30,
    exposure=0.5,
    timestamp_jitter_sd=0.002,  # the camera
)
clip_dir = write_impact_clip(
    spec, "~/clips"
)  # clip.mp4, truth.json, keypoints.ndjson, trajectory.csv, scene.json
write_impact_clip(
    spec, "~/clips", render=False
)  # truth + keypoints only: no browser needed
write_impact_set("~/clips/set")  # 24 clips sharing one performance
```

The clips are ordinary `an` scenes (two props, one tween per stroke segment), and the ground truth is read back from the same compiled document the renderer draws — `write_impact_clip` refuses to write a sidecar whose keypoints disagree with the analytic motion at any captured instant, or whose document is not the one the renderer staged. With an open shutter, a frame’s keypoints are the average over its exposure (what the blurred frame shows), with the mid-exposure position recorded beside them. The `truth.json` schema is documented in `cutan/impacts/truth.py`. The camera model is `an.frame_clock.FrameClock`, which reaches the renderer through `RenderContext.frame_samples`: several instants averaged per frame are an open shutter, instants off the `i / fps` grid are capture jitter, and a render without it is byte-identical to before.

---

## What’s not in v0.1

- 3D animation, generative video, interactive output, SaaS hosting, music/sound-effect generation, in-house GUI, or editing of pre-existing video footage. `an` synthesizes; it does not cut.
- The Manim backend works for placeholder title cards but isn’t doing real shot-to-Manim translation; Remotion + whiteboard are skeleton implementations that respond correctly to `can_render` but can’t produce video yet.
- Lip-sync for face-baked characters. Characters whose descriptor declares
  `face_overlay: false` (DiceBear avatars, or any hand-declared baked face)
  carry their face inside the head SVG, so the
  compiler suppresses both the overlay mouth and the viseme channel — they speak
  without moving their mouths. Hand-rigged characters lip-sync normally; see
  `examples/promote_demo/`.
- Multi-scene projects — `"main"` is the only scene key supported today.

SVG character art *has* shipped — descriptors drive real sprites through
`svg_sprite` visuals, not the placeholder geometry an earlier version of this
list described.

---

## Reference

- [`misc/docs/architecture_as_built.md`]() — module map, control flows, invariants, caching strategy.
- [`misc/docs/`]() — the design-space research reports, plus
  [`wave1_verification.md`](), which is *verified current
  fact* rather than design space: licences with the URLs they were read at, and the
  silent-discard inventory.
- [`misc/CHANGELOG.md`]() — phase-by-phase what shipped when.
- [`.claude/skills/`]() — the skills the agent uses to drive the package
  (`an` to use it, `an-spec` to interview a director, `an-dev` to work on it, plus
  `an-dev-licensing` and `an-dev-runtime-assets` for the two things that fail silently).
- [`examples/`]() — the canonical demo scenes. `single_character/` is the smallest
  thing that renders; `character_gallery/build.py` goes end to end from character creation.

<p class="epythet-aggregates">This documentation as a single file: <a href="an.md">an.md</a> (Markdown, for agents).</p>


# _autosummary/an.adapters.cutout.cache_key.html.md

# an.adapters.cutout.cache_key

Moved to [`an.stage.cache_key`](_autosummary/an.stage.cache_key.html.md#module-an.stage.cache_key) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.canvas_capture.html.md

# an.adapters.cutout.canvas_capture

Moved to [`an.stage.canvas_capture`](_autosummary/an.stage.canvas_capture.html.md#module-an.stage.canvas_capture) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.channel.html.md

# an.adapters.cutout.channel

Channel evaluation — moved to [`an.timing.channel`](_autosummary/an.timing.channel.html.md#module-an.timing.channel) (the timing kernel).

This path keeps working for every existing caller; new code imports from
`an.timing`. \*\*This module’s names are the executable spec of
`runtime.js`’s `evaluateChannel``** (with the default ``kind=None`: by value
type), pinned by `tests/test_cutout_channel_parity.py`.

```pycon
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> evaluate(ch, 0.5)
5.0
```

### Functions

| [`evaluate`](_autosummary/an.adapters.cutout.channel.html.md#an.adapters.cutout.channel.evaluate)(channel, t, \*[, kind])   | Evaluate `channel` at time `t` (see the module docstring for `kind`).   |
|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`check_channel`](_autosummary/an.adapters.cutout.channel.html.md#an.adapters.cutout.channel.check_channel)(channel, kind)       | Why `channel`'s keyframe values do not fit `kind` (empty if they do).   |

### Classes

| [`Channel`](_autosummary/an.adapters.cutout.channel.html.md#an.adapters.cutout.channel.Channel)(target, property[, keyframes])   | Sorted keyframes for one property of one target.        |
|-------------------------------------------------------------------------------------------|---------------------------------------------------------|
| [`Keyframe`](_autosummary/an.adapters.cutout.channel.html.md#an.adapters.cutout.channel.Keyframe)(time, value[, easing])          | One keyframe: time, value, optional per-segment easing. |

### *class* an.adapters.cutout.channel.Channel(target, property, keyframes=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Sorted keyframes for one property of one target.

Construction validates that `keyframes` is non-empty and sorted.

### *class* an.adapters.cutout.channel.Keyframe(time, value, easing=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One keyframe: time, value, optional per-segment easing.

The easing on a keyframe describes the curve **leaving** that keyframe
toward the next one. The last keyframe’s easing is therefore unused.

### an.adapters.cutout.channel.check_channel(channel, kind)

Why `channel`’s keyframe values do not fit `kind` (empty if they do).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.adapters.cutout.channel.evaluate(channel, t, , kind=None)

Evaluate `channel` at time `t` (see the module docstring for `kind`).

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)


# _autosummary/an.adapters.cutout.clip.html.md

# an.adapters.cutout.clip

Clips, loop modes and poses — moved to [`an.timing.clip`](_autosummary/an.timing.clip.html.md#module-an.timing.clip) (the timing kernel).

This path keeps working for every existing caller; new code imports from
`an.timing`.

```pycon
>>> from an.adapters.cutout.channel import Channel, Keyframe
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> clip = Clip("walk", duration=1.0, channels=[ch], loop_mode=LoopMode.LOOP)
>>> evaluate(clip, 1.25)[("a", "x")]  # loop wraps
2.5
```

### Functions

| [`evaluate`](_autosummary/an.adapters.cutout.clip.html.md#an.adapters.cutout.clip.evaluate)(clip, t, \*[, kind_of])   | Evaluate `clip` at time `t`, returning a `Pose`.                       |
|-------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`merge_poses`](_autosummary/an.adapters.cutout.clip.html.md#an.adapters.cutout.clip.merge_poses)(\*poses)               | Merge multiple poses with **override semantics** (later wins per key). |

### Classes

| [`Clip`](_autosummary/an.adapters.cutout.clip.html.md#an.adapters.cutout.clip.Clip)(name, duration[, channels, loop_mode])   | Named animation: a duration + a bundle of channels.   |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------|
| [`LoopMode`](_autosummary/an.adapters.cutout.clip.html.md#an.adapters.cutout.clip.LoopMode)(\*values)                            | How a clip behaves past its natural duration.         |

### *class* an.adapters.cutout.clip.Clip(name, duration, channels=<factory>, loop_mode=LoopMode.ONCE)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Named animation: a duration + a bundle of channels.

### *class* an.adapters.cutout.clip.LoopMode(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

How a clip behaves past its natural duration.

### an.adapters.cutout.clip.evaluate(clip, t, , kind_of=None)

Evaluate `clip` at time `t`, returning a `Pose`.

`kind_of` declares each channel’s field kind; `None` interpolates by
value type, as `runtime.js` does (see [`an.timing.channel`](_autosummary/an.timing.channel.html.md#module-an.timing.channel)).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.adapters.cutout.clip.merge_poses(\*poses)

Merge multiple poses with **override semantics** (later wins per key).

Used by the timeline to combine concurrent clips on the same target.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> merge_poses({("a", "x"): 1.0}, {("a", "x"): 2.0, ("a", "y"): 3.0})
{('a', 'x'): 2.0, ('a', 'y'): 3.0}
```


# _autosummary/an.adapters.cutout.compile.html.md

# an.adapters.cutout.compile

Moved to [`an.stage.compile`](_autosummary/an.stage.compile.html.md#module-an.stage.compile) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.easing.html.md

# an.adapters.cutout.easing

Moved to [`an.stage.easing`](_autosummary/an.stage.easing.html.md#module-an.stage.easing) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.fidelity.html.md

# an.adapters.cutout.fidelity

Moved to [`an.stage.fidelity`](_autosummary/an.stage.fidelity.html.md#module-an.stage.fidelity) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.html.md

# an.adapters.cutout

The cut-out backend’s old package: the stage moved to [`an.stage`](_autosummary/an.stage.html.md#module-an.stage) (an#247).

The render path is `an.stage.compile` -> `an.stage.serialize` ->
`an.stage.render` (the stage engine, driven by the core frame stage) ->
`runtime.js`. Every module that moved keeps a LIVE alias here
(`an.adapters.cutout.render` is `an.stage.render` for reading and for
rebinding, `an._shims.alias_module()`). `coarticulate` and `gaze` moved to `cutan` (an#225; live aliases with a
warning). What stays here is the
timing re-exports (`channel`, `clip`, `timeline`).

The names this package used to export are resolved LAZILY, on first access:
importing `an.adapters.cutout.coarticulate` from the stage’s compiler must not
load the stage back through this `__init__`.

```pycon
>>> from an.adapters.cutout import CutoutRenderer, compile_shot
>>> CutoutRenderer().name
'cutout'
```

### Functions

| [`compile_shot`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.compile_shot)(shot[, mall, fps, width, ...])   | Compile a single cutout-style `Shot` to its JS-runtime JSON form.   |
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|

### Classes

| [`CutoutRenderer`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.CutoutRenderer)([engine, name, ...])   | The stage renderer: the stage engine through the core frame stage.        |
|----------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`CutoutSceneJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.CutoutSceneJSON)(\*\*data)             | Top-level cutout scene JSON — the JS runtime's input contract.            |
| [`NodeJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.NodeJSON)(\*\*data)                    | One node in the scene tree.                                               |
| [`VisualJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.VisualJSON)(\*\*data)                  | Drawable content attached to a node.                                      |
| [`AnimationClipJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.AnimationClipJSON)(\*\*data)           | A named, reusable animation clip.                                         |
| [`ChannelJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.ChannelJSON)(\*\*data)                 | One animated property of one target.                                      |
| [`KeyframeJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.KeyframeJSON)(\*\*data)                | Single keyframe in an animation channel.                                  |
| [`TimelineJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.TimelineJSON)(\*\*data)                | Top-level timeline: total duration + tracks.                              |
| [`TrackJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.TrackJSON)(\*\*data)                   | A sequence of placed clips with optional target-prefix metadata.          |
| [`PlacedClipJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.PlacedClipJSON)(\*\*data)              | An animation placed on a track at a specific time.                        |
| [`AssetsJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.AssetsJSON)(\*\*data)                  | Map of asset id → AssetJSON, split by kind.                               |
| [`AssetJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.AssetJSON)(\*\*data)                   | A single asset (texture / audio file).                                    |
| [`AssetResolutionJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.AssetResolutionJSON)(\*\*data)         | How one scene entity's store reference actually resolved at compile time. |

### Exceptions

| [`CutoutRenderError`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.CutoutRenderError)   | Raised when a cutout render fails.   |
|----------------------------------------------------------------------|--------------------------------------|

### *class* an.adapters.cutout.AnimationClipJSON(\*\*data)

Bases: `_JSONModel`

A named, reusable animation clip.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.adapters.cutout.AssetJSON(\*\*data)

Bases: `_JSONModel`

A single asset (texture / audio file).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.adapters.cutout.AssetResolutionJSON(\*\*data)

Bases: `_JSONModel`

How one scene entity’s store reference actually resolved at compile time.

The IR declares every drawable entity as `store` + `ref`. What the
compiler *builds* from that pair is not recoverable from the scene tree
afterwards: a character whose descriptor is missing and a character that
never had one produce byte-identical procedural rigs. That ambiguity is
an#33 — three CI runners once agreed perfectly about a picture that was not
the picture, and the agreement read as a clean positive result.

So the compiler records what it did, per entity, in the artifact the
browser actually loads. `fallback` is the load-bearing bit: True means
the declared ref supplied nothing and a stand-in was drawn in its place.

```pycon
>>> AssetResolutionJSON(
...     id="maya", kind="character", store="characters", ref="maya-v1",
...     resolved="descriptor", fallback=False,
... ).fallback
False
```

#### detail *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

One human sentence saying why, when `fallback` is True.

#### fallback *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when the declared ref supplied nothing and a stand-in was drawn.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### resolved *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

“descriptor” | “parts” | “placeholder”.
Environments: “store” | “preset” | “default”.

* **Type:**
  What was built. Characters

### *class* an.adapters.cutout.AssetsJSON(\*\*data)

Bases: `_JSONModel`

Map of asset id → AssetJSON, split by kind.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.adapters.cutout.ChannelJSON(\*\*data)

Bases: `_JSONModel`

One animated property of one target.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *exception* an.adapters.cutout.CutoutRenderError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a cutout render fails. Carries actionable detail.

### *class* an.adapters.cutout.CutoutRenderer(engine=<factory>, name='cutout', supported_renderers=('cutout', 'stage'), error=<class 'an.stage.render.CutoutRenderError'>, capture_options=<factory>, paint_orders=('container', 'global'))

Bases: [`FrameStageRenderer`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.FrameStageRenderer)

The stage renderer: the stage engine through the core frame stage.

It claims both renderer names (ADR 0001 decision 9): `stage`, the
engine’s own, and `cutout`, the persisted name every existing scene
carries. Its registry name stays `cutout` – persisted too (the shot
cache keys on it) – and `StageRenderer` is the same class.

```pycon
>>> r = CutoutRenderer()
>>> r.name
'cutout'
>>> r.supported_renderers
('cutout', 'stage')
```

#### error

alias of [`CutoutRenderError`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderError)

#### paint_orders *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('container', 'global')*

one container’s
items sorted (`applyPaintOrder`) and one list across containers
(`applyGlobalPaint`). Read as the `engine.paint_order` capability.

* **Type:**
  The paint orders the stage runtime honours (an#430)

#### supported_renderers *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('cutout', 'stage')*

The `Shot.renderer` values this renderer claims (the ONE place it names them).

### *class* an.adapters.cutout.CutoutSceneJSON(\*\*data)

Bases: `_JSONModel`

Top-level cutout scene JSON — the JS runtime’s input contract.

Versioned so the runtime can refuse incompatible inputs.

#### asset_resolution *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[AssetResolutionJSON](_autosummary/an.stage.serialize.html.md#an.stage.serialize.AssetResolutionJSON)]*

One entry per drawable entity, in scene order — see
[`AssetResolutionJSON`](_autosummary/an.adapters.cutout.html.md#an.adapters.cutout.AssetResolutionJSON). Inert to the runtime; read by the bench
harness and the golden-corpus bless to assert WHICH render path ran.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### overlay *: [NodeJSON](_autosummary/an.stage.serialize.html.md#an.stage.serialize.NodeJSON) | [None](https://docs.python.org/3/builtins/constants.html#None)*

a second top-level container the
runtime centres on the canvas and never indexes, so no channel — the
camera’s `root.pivot`/`root.scale` included — can reach it. Its
children are indexed by their own paths exactly like `scene`’s, so an
overlay title’s words tween like anything else. `None` when the shot
has no overlay, and then omitted from the serialized document.

* **Type:**
  The camera-immune layer (an#155)

### *class* an.adapters.cutout.KeyframeJSON(\*\*data)

Bases: `_JSONModel`

Single keyframe in an animation channel.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.adapters.cutout.NodeJSON(\*\*data)

Bases: `_JSONModel`

One node in the scene tree.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### paint_order *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The order its descendants’ visuals are PAINTED in, when it is not the
tree’s (an#430): node paths relative to this node, back to front. Set on
a rig’s entity node whose nested chain interleaves with an unrelated
part (an upper arm 1, a cord 2, a forearm 3); the runtime keeps each
visual in its transform tree and paints it from this list.

#### scope *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

as the children of the
node named `scope` in the same parent (`""`: as the parent’s own
children). `None` = under this node’s own path, the rule before
an#343. An environment’s foreground container carries `scope=<env>`,
so every plane is `<env>/<plane>` wherever the environment is cut.
[`an.stage.tree`](_autosummary/an.stage.tree.html.md#module-an.stage.tree) is the Python statement of the rule.

* **Type:**
  Where this node’s CHILDREN are indexed (an#343)

#### z_index *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

This node’s place among its siblings when its parent sorts them
(an#403): a nested chain whose declared draw order is not the tree’s
(a far arm behind the torso it nests under). `None` = insertion order.

### *class* an.adapters.cutout.PlacedClipJSON(\*\*data)

Bases: `_JSONModel`

An animation placed on a track at a specific time.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.adapters.cutout.TimelineJSON(\*\*data)

Bases: `_JSONModel`

Top-level timeline: total duration + tracks.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.adapters.cutout.TrackJSON(\*\*data)

Bases: `_JSONModel`

A sequence of placed clips with optional target-prefix metadata.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.adapters.cutout.VisualJSON(\*\*data)

Bases: `_JSONModel`

Drawable content attached to a node.

`kind="svg_sprite"` is the Phase 11b path: the runtime instantiates a
`PIXI.Sprite` from a pre-loaded SVG texture identified by `asset_id`.

`asset_sets` carries this node’s swap vocabulary —
`{set_name: {KEY: asset_id}}`, the compiler’s per-slot **projection** of
the descriptor’s `asset_sets` onto the slot this visual draws (an#87).
A channel whose property names one of these sets swaps the sprite’s
texture by key; `viseme` is just the conventional set name lip-sync
uses. Same field name as `CharacterDescriptor.asset_sets` on purpose:
one vocabulary, two layers (descriptor = declared, wire = resolved to
texture aliases). Replaces the mouth-only `viseme_assets`.

`width`/`height` are the box the art is fitted **into**, not the size it
is forced to. Under `fit="contain"` the art keeps its own aspect ratio and
may leave slack on one axis; that slack is the correct rendering, not a bug.

#### asset_geometry *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [float](https://docs.python.org/3/builtins/functions.html#float)]] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per-texture geometry for a swap key drawn differently from the built
one (an#211): `{asset_id: {"width", "height", "anchor_x", "anchor_y",
"x", "y"}}` — the box the key is fitted into, its anchor, and its
offset from the node (scene pixels). A swap used to carry the texture
only, so every key drew in the DEFAULT attachment’s box: a closed mouth
on a thin canvas squashed every open mouth to a fraction of a pixel. Only
keys whose geometry differs are listed; `None` = every key shares it.

#### blend *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['add', 'multiply'] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`"add"` for a
glow, `"multiply"` for the paper grain. PixiJS 7 does both in the
blend equation — no filter, no render texture. `None` = normal.

* **Type:**
  The engine’s native blend mode for this visual (an#163)

#### fit *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['stretch', 'contain']*

How the art is fitted to `width`/`height`.

`"contain"` scales uniformly so the art keeps the shape it was drawn
with — the invariant of an#74. `"stretch"` sizes each axis
independently, which is what every sprite did before that issue and what
distorted `arm_l` by 3.929x on the repo’s own art.

Additive with a `"stretch"` default so no stored scene changes meaning;
the compiler emits `"contain"` for every sprite it builds.

#### kind *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

One of `BUILTIN_VISUAL_KINDS`, or a kind a genre’s runtime script
registers (`window.anRegisterVisual`, an#247). A string on the wire, as
it always was; the open set is what lets `cutan` ship the mouth and eye.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### path *: [PathJSON](_autosummary/an.stage.serialize.html.md#an.stage.serialize.PathJSON) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The stroke for `kind="path"` (an#160); `None` on every other visual.

#### underlays *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[UnderlayJSON](_autosummary/an.stage.serialize.html.md#an.stage.serialize.UnderlayJSON)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Copies drawn behind this visual, back to front (an#163) — the outline
and the paper-gap shadow. Only `rect`, `ellipse` and `svg_sprite`
take them; the runtime refuses any other kind.

#### z_index *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The visual’s place among its node’s children when the node sorts them
(an#403): a part drawn over the chain nested under it.

### an.adapters.cutout.compile_shot(shot, mall=None, , fps=30, width=1920, height=1080, background='#ffffff', strict_assets=False, step_hz=None, expression_provider=None, style_pack=None, default_easing=None)

Compile a single cutout-style `Shot` to its JS-runtime JSON form.

`default_easing` (an#166) is the scene’s `meta.default_easing`: the
curve of every authored tween that names none (tween > this > the built-in
`"ease_in_out"`, [`resolved_easing()`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction.resolved_easing)).
`None` leaves the document byte-identical to before the knob existed.

`expression_provider` (an#98) is the seam that turns authored
`expression` leaves and dialogue `[emotion]` sugar into per-axis
curves for the face solver; `None` is the genre’s default provider.

`step_hz` (an#89) resamples every authored **tween** onto a SHOT-wide
pose grid of that many updates per second (multiples of `1/step_hz` on
this shot’s clock, shared by every tween in the shot; the grid restarts at
a cut), each keyframe step-eased, so the character holds each pose for the
frames between grid points — “on twos” at half the frame rate, “on threes”
at a third. It is sample-and-hold of the eased curve at the grid instants,
not a retiming into holds and fast transitions. `None` (default) leaves
tweens smooth and the compiled document byte-identical to before the knob
existed; anything else must satisfy `0 < step_hz <= fps` — checked HERE
as well as by `an validate`, because a render never runs validate and a
non-positive rate used to spin `step_times` forever (an#89 review).
Exempt by construction, because they are separate
emission sites rather than string-sniffed: the camera (`_add_camera_clips`
— a stepped character under a translating camera slides in screen space,
which is why the practice keeps cameras on ones), compiled blinks, `play`
clips, and swap channels (already stepped by format). The value is
stamped into `meta.step_hz` — only when set — so a serialized scene
declares its timing policy without moving the contract hash of one that
has none.

`strict_assets` turns a stand-in asset — the placeholder rig drawn for a
character whose descriptor is missing, or the default backdrop drawn for an
unknown environment ref — from a warning into a `CutoutCompileError`.
Off by default so an asset-less project still renders; on for anything that
measures pixels, where a stand-in is a wrong answer wearing a right one’s
clothes (an#33).

* **Return type:**
  [`CutoutSceneJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.CutoutSceneJSON)

### Modules

| [`cache_key`](_autosummary/an.adapters.cutout.cache_key.html.md#module-an.adapters.cutout.cache_key)           | Moved to [`an.stage.cache_key`](_autosummary/an.stage.cache_key.html.md#module-an.stage.cache_key) (an#247); this path is a LIVE alias of it.           |
|----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`canvas_capture`](_autosummary/an.adapters.cutout.canvas_capture.html.md#module-an.adapters.cutout.canvas_capture) | Moved to [`an.stage.canvas_capture`](_autosummary/an.stage.canvas_capture.html.md#module-an.stage.canvas_capture) (an#247); this path is a LIVE alias of it. |
| [`channel`](_autosummary/an.adapters.cutout.channel.html.md#module-an.adapters.cutout.channel)               | Channel evaluation — moved to [`an.timing.channel`](_autosummary/an.timing.channel.html.md#module-an.timing.channel) (the timing kernel).              |
| [`clip`](_autosummary/an.adapters.cutout.clip.html.md#module-an.adapters.cutout.clip)                     | Clips, loop modes and poses — moved to [`an.timing.clip`](_autosummary/an.timing.clip.html.md#module-an.timing.clip) (the timing kernel).           |
| [`compile`](_autosummary/an.adapters.cutout.compile.html.md#module-an.adapters.cutout.compile)               | Moved to [`an.stage.compile`](_autosummary/an.stage.compile.html.md#module-an.stage.compile) (an#247); this path is a LIVE alias of it.               |
| [`easing`](_autosummary/an.adapters.cutout.easing.html.md#module-an.adapters.cutout.easing)                 | Moved to [`an.stage.easing`](_autosummary/an.stage.easing.html.md#module-an.stage.easing) (an#247); this path is a LIVE alias of it.                 |
| [`fidelity`](_autosummary/an.adapters.cutout.fidelity.html.md#module-an.adapters.cutout.fidelity)             | Moved to [`an.stage.fidelity`](_autosummary/an.stage.fidelity.html.md#module-an.stage.fidelity) (an#247); this path is a LIVE alias of it.             |
| [`path`](_autosummary/an.adapters.cutout.path.html.md#module-an.adapters.cutout.path)                     | Moved to [`an.stage.path_geometry`](_autosummary/an.stage.path_geometry.html.md#module-an.stage.path_geometry) (an#247); this path is a LIVE alias of it.   |
| [`render`](_autosummary/an.adapters.cutout.render.html.md#module-an.adapters.cutout.render)                 | Moved to [`an.stage.render`](_autosummary/an.stage.render.html.md#module-an.stage.render) (an#247); this path is a LIVE alias of it.                 |
| [`runtime_files`](_autosummary/an.adapters.cutout.runtime_files.html.md#module-an.adapters.cutout.runtime_files)   | Moved to [`an.stage.runtime_files`](_autosummary/an.stage.runtime_files.html.md#module-an.stage.runtime_files) (an#247); this path is a LIVE alias of it.   |
| [`serialize`](_autosummary/an.adapters.cutout.serialize.html.md#module-an.adapters.cutout.serialize)           | Moved to [`an.stage.serialize`](_autosummary/an.stage.serialize.html.md#module-an.stage.serialize) (an#247); this path is a LIVE alias of it.           |
| [`shutter`](_autosummary/an.adapters.cutout.shutter.html.md#module-an.adapters.cutout.shutter)               | Moved to [`an.media.shutter`](_autosummary/an.media.shutter.html.md#module-an.media.shutter) (an#247); this path is a LIVE alias of it.               |
| [`supersample`](_autosummary/an.adapters.cutout.supersample.html.md#module-an.adapters.cutout.supersample)       | Moved to [`an.media.supersample`](_autosummary/an.media.supersample.html.md#module-an.media.supersample) (an#247); this path is a LIVE alias of it.       |
| [`surface`](_autosummary/an.adapters.cutout.surface.html.md#module-an.adapters.cutout.surface)               | Moved to [`an.stage.surface`](_autosummary/an.stage.surface.html.md#module-an.stage.surface) (an#247); this path is a LIVE alias of it.               |
| [`text`](_autosummary/an.adapters.cutout.text.html.md#module-an.adapters.cutout.text)                     | Moved to [`an.stage.text_layout`](_autosummary/an.stage.text_layout.html.md#module-an.stage.text_layout) (an#247); this path is a LIVE alias of it.       |
| [`timeline`](_autosummary/an.adapters.cutout.timeline.html.md#module-an.adapters.cutout.timeline)             | Moved to [`an.stage.timeline`](_autosummary/an.stage.timeline.html.md#module-an.stage.timeline) (an#247); this path is a LIVE alias of it.             |


# _autosummary/an.adapters.cutout.path.html.md

# an.adapters.cutout.path

Moved to [`an.stage.path_geometry`](_autosummary/an.stage.path_geometry.html.md#module-an.stage.path_geometry) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.render.html.md

# an.adapters.cutout.render

Moved to [`an.stage.render`](_autosummary/an.stage.render.html.md#module-an.stage.render) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.runtime_files.html.md

# an.adapters.cutout.runtime_files

Moved to [`an.stage.runtime_files`](_autosummary/an.stage.runtime_files.html.md#module-an.stage.runtime_files) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.serialize.html.md

# an.adapters.cutout.serialize

Moved to [`an.stage.serialize`](_autosummary/an.stage.serialize.html.md#module-an.stage.serialize) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.shutter.html.md

# an.adapters.cutout.shutter

Moved to [`an.media.shutter`](_autosummary/an.media.shutter.html.md#module-an.media.shutter) (an#247); this path is a LIVE alias of it.

The resolve is engine-independent, so it lives in the core’s media package.
Every name of the new module is reachable here, and rebinding one here rebinds
it there (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.supersample.html.md

# an.adapters.cutout.supersample

Moved to [`an.media.supersample`](_autosummary/an.media.supersample.html.md#module-an.media.supersample) (an#247); this path is a LIVE alias of it.

The resolve is engine-independent, so it lives in the core’s media package.
Every name of the new module is reachable here, and rebinding one here rebinds
it there (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.surface.html.md

# an.adapters.cutout.surface

Moved to [`an.stage.surface`](_autosummary/an.stage.surface.html.md#module-an.stage.surface) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.text.html.md

# an.adapters.cutout.text

Moved to [`an.stage.text_layout`](_autosummary/an.stage.text_layout.html.md#module-an.stage.text_layout) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.adapters.cutout.timeline.html.md

# an.adapters.cutout.timeline

Moved to [`an.stage.timeline`](_autosummary/an.stage.timeline.html.md#module-an.stage.timeline) (an#247); this path is a LIVE alias of it.

The compiled document’s evaluation entry point and its screen space are the
stage’s (ADR 0001 decision 5). Every name of the new module is reachable here,
and rebinding one here rebinds it there (`an._shims.alias_module()`).


# _autosummary/an.adapters.html.md

# an.adapters

Renderer adapters — facades over backends (the stage, Manim, Remotion, whiteboard).

The Renderer Protocol and registry live in `_base`. The core’s own optional
backends are imported here so they self-register on package import. The stage
(`an.stage`, the cut-out renderer) sits behind the import firewall, so it is
named by MODULE and imported the first time the registry is asked anything
(an#247). Backends with missing
system deps still register but their `render()` raises a clear error;
`can_render(shot)` continues to work for routing decisions.

### Functions

| [`register_renderer`](_autosummary/an.adapters.html.md#an.adapters.register_renderer)(renderer)          | Register a renderer in the default registry.                               |
|---------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`get_renderer`](_autosummary/an.adapters.html.md#an.adapters.get_renderer)(name)                   | Look up a renderer by name in the default registry.                        |
| [`list_renderers`](_autosummary/an.adapters.html.md#an.adapters.list_renderers)()                     | Names of all renderers registered in the default registry.                 |
| [`register_lazy_renderer`](_autosummary/an.adapters.html.md#an.adapters.register_lazy_renderer)(name, module) | Name the module whose import registers renderer `name` (default registry). |

### Classes

| [`Renderer`](_autosummary/an.adapters.html.md#an.adapters.Renderer)(\*args, \*\*kwargs)                       | Backend renderer interface.                                                  |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`RendererRegistry`](_autosummary/an.adapters.html.md#an.adapters.RendererRegistry)(\*[, entry_point_group])          | Name-keyed registry of renderers.                                            |
| [`RenderContext`](_autosummary/an.adapters.html.md#an.adapters.RenderContext)(mall, work_dir[, fps, ...])          | Everything a renderer needs that isn't on the Shot itself.                   |
| [`RenderResult`](_autosummary/an.adapters.html.md#an.adapters.RenderResult)(mp4_path, duration[, ...])            | Outcome of a single shot render.                                             |
| [`ManimRenderer`](_autosummary/an.adapters.html.md#an.adapters.ManimRenderer)(\*[, render_check, source_resolver]) | Manim Community Edition, through `manimkit`: an opaque-source shot renderer. |
| [`RemotionRenderer`](_autosummary/an.adapters.html.md#an.adapters.RemotionRenderer)()                                 | Remotion-based renderer (skeleton).                                          |
| [`WhiteboardRenderer`](_autosummary/an.adapters.html.md#an.adapters.WhiteboardRenderer)()                               | Whiteboard-style renderer (stub).                                            |

### *class* an.adapters.ManimRenderer(, render_check=None, source_resolver=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Manim Community Edition, through `manimkit`: an opaque-source shot renderer.

Implements [`Renderer`](_autosummary/an.adapters.html.md#an.adapters.Renderer) and
`ClockOwningRenderer`. Seams: `render_check`
(default `manimkit.render_check()`, imported on first use) and
`source_resolver` (default: the project’s `sources` store,
`store_source_resolver()`). The shot cache keys a shot through the
REGISTERED instance’s resolver; a subclass registers its own keyer
(`register_shot_keyer(name, manim_shot_inputs, renderer_type=Sub)`).

#### measure_duration(shot, ctx, , render=True, force=False)

Manim’s length of this shot’s scene, from the `measurements` store,
or rendered now (and stored) when there is none — or when `force`.

* **Return type:**
  `DurationMeasurement` | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### probe_frames(shot, ctx, times)

The film’s frames of `shot` at each of `times`, from its STORED picture (an#347).

Manim owns its clock and renders a whole scene at once, so `an
probe` never runs it: the picture a render stored is conformed
exactly as [`render()`](_autosummary/an.adapters.html.md#an.adapters.ManimRenderer.render) conforms it, and the frames showing at
`times` are read out. With no stored picture it refuses, naming the
render that stores one.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

#### render(shot, ctx)

Render `shot` for exactly `shot.duration` (`an.render` settles it
to the measured length, or longer to hold for its narration).

* **Return type:**
  [`RenderResult`](_autosummary/an.adapters.html.md#an.adapters.RenderResult)

#### stored_reads(key, ctx)

The read trace stored with picture `key` (`None`: none stored,
or not recorded). Reads stores only; never renders.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* an.adapters.RemotionRenderer

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Remotion-based renderer (skeleton).

### *class* an.adapters.RenderContext(mall, work_dir, fps=30, resolution=(1920, 1080), strict_assets=False, supersample=1, pix_fmt=None, step_hz=None, style_pack=None, default_easing=None, frame_samples=None, capture=None, extra=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything a renderer needs that isn’t on the Shot itself.

`mall` carries the project’s stores so the renderer can resolve assets
by reference. `work_dir` is a scratch space; the renderer must clean up
after itself or treat it as ephemeral.

#### capture *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`"screenshot"` (a Playwright element
screenshot per instant) or `"canvas"` (the runtime reads its own canvas
in-page, in batches). `None` is the renderer’s module default, read at
call time — `"canvas"` since an#192, after the equivalence gate held on
the whole corpus on both lanes. The two paths write frames whose DECODED
pixels are equal, so this is a throughput knob and never a picture knob;
a `RenderContext` field for `supersample`’s reason, and recorded in
per-shot provenance.

* **Type:**
  How frames leave the browser

#### default_easing *: [Any](https://docs.python.org/3/library/typing.html#typing.Any)*

the curve of every
authored tween that names none. `None` = the built-in
`"ease_in_out"`, and a compiled document byte-identical to before the
field existed; set, it changes keyframes, so the contract hash moves
with it — as it should.

* **Type:**
  The scene’s `meta.default_easing` (an#166)

#### fps *: [int](https://docs.python.org/3/builtins/functions.html#int) | [float](https://docs.python.org/3/builtins/functions.html#float)*

Frames per second of the delivered video. May be non-integer — a
camera’s 29.97 — and the capture loop and the mux honour it exactly. The
COMPILER aligns its frame-sampled curves (gaze saccades, co-articulation,
descriptor plays, face curves) to the nearest integer grid instead,
because the compiled document’s `meta.fps` is an integer; tweens and
every other keyframe are exact at any rate.

#### frame_samples *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), ...], ...] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per output frame, the scene instants to render and average into it —
`None` is one instant at `i / fps`, the path every render took before
this field existed, byte for byte. Several instants per frame are an open
shutter (motion blur); instants off the `i / fps` grid are capture
jitter. Built by [`an.frame_clock.FrameClock`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock), which is also what
the impact harness writes into its ground truth, so the render and the
record of when each frame was taken come from one object.

A `RenderContext` field for `supersample`’s reason: it changes how frames
are CAPTURED, not what the scene is, so it must not move the compiled
document. Its length must equal the render’s frame count.

#### pix_fmt *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The delivered encode’s pixel format, or `None` for the module default.
**The one first-order quality lever in the encoder**: 4:2:0 -> 4:4:4 cuts
the edge-band error 11.35 -> 3.79, where mathematically lossless 4:2:0
only reaches 10.15. Losslessness buys 8%; dropping chroma subsampling
buys 66%.

`None` rather than the literal, so the bench’s `pix_fmt` lever — which
rebinds the module default — still reaches an unset render. The default
stays 4:2:0 for a PRODUCT reason and not an encoder one: High 4:4:4
Predictive is refused by many hardware decoders, browsers and platforms.

#### step_hz *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Scene-level stepped-timing policy for authored tweens (an#89); a shot’s
own `step_hz` overrides it. `None` = smooth. Reaches the compiled
document’s `meta.step_hz` (only when set) and per-shot provenance.

The one deliberate exception to the rule two fields up (“a field on the
compiled scene document moves `scene_contract_sha256`”): unlike
`supersample`, this knob CHANGES the compiled document — the resampled
keyframes are the contract — so the hash moves whenever it is set no
matter where the knob lives, and a document that carries its own timing
policy is the honest one. Omit-when-unset keeps the unset case free.

#### strict_assets *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Refuse to draw a stand-in for a declared asset that the stores do not
supply. Off by default so an asset-less project still renders; on for
anything that measures pixels, where a stand-in is a different picture
that looks like a successful render (an#33).

#### style_pack *: [StylePack](_autosummary/an.styles.html.md#an.styles.StylePack) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The `StylePack` this render is drawn under, already resolved from the
scene’s `meta.style_pack` (an#112). Resolved ONCE per render rather than
per shot: a pack is art direction for a project, and a scene whose shots
disagreed about it would be two scenes.

#### supersample *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Render at this many times the declared resolution and resolve back with
an exact block mean. **1 means off, and off is free** — Chromium’s own
PNG bytes reach disk untouched.

A `RenderContext` field, and that placement is load-bearing rather than
convenient. Simulated against a real committed ledger row: as a
`render_kwargs` entry it becomes a `COMMON_ENV_PATHS` key and \*\*all 96
metrics are refused\*\*; as a field on the compiled scene document it moves
`scene_contract_sha256` and **every scene becomes incomparable**; here,
only `runtime_sha256` moves, which is deliberately not a comparability
key — so 30 render-side entries still compare. It needs no
`SCHEMA_VERSION` migration, and it MUST reach per-shot provenance, because
a row that does not record it cannot be read back later.

### *class* an.adapters.RenderResult(mp4_path, duration, frame_manifest=<factory>, log='', provenance=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of a single shot render.

### *class* an.adapters.Renderer(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Backend renderer interface.

Implementations should be cheap to construct and stateless across renders;
state belongs in the `RenderContext` or the project mall.

#### can_render(shot)

Return True if this renderer can render `shot`.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### render(shot, ctx)

Render a single shot to mp4. Idempotent given identical inputs.

* **Return type:**
  [`RenderResult`](_autosummary/an.adapters.html.md#an.adapters.RenderResult)

#### supported_renderers *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

The `Shot.renderer` values this backend claims. It is the ONE place
an adapter names them: `can_render` derives from it rather than
comparing to its own literal, so an adapter cannot advertise one
renderer and accept another. an#106 renamed this from
`supported_styles`; the rename is a break for an out-of-tree adapter
because `Renderer` is `@runtime_checkable` and 3.12 checks data
members, so `isinstance(old_adapter, Renderer)` is now False.

### *class* an.adapters.RendererRegistry(, entry_point_group=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Name-keyed registry of renderers.

A module-level instance is exposed via `register_renderer` /
`get_renderer` / `list_renderers`; callers needing isolation (tests,
multi-tenant servers) can construct their own.

**Backends outside the core register LAZILY** (an#247): a renderer that
lives behind the import firewall – the stage, a genre’s, a third-party
engine – is named here by the MODULE that registers it
([`register_lazy()`](_autosummary/an.adapters.html.md#an.adapters.RendererRegistry.register_lazy), or the `an.renderers` entry point group), and
that module is imported the first time the registry is asked anything. So
importing the core loads no backend, and every lookup still finds it.

#### find_for(shot)

Return the first registered renderer that `can_render(shot)`.

`None` when none can; a `RendererLoadError` when none can AND
a backend failed to import, since that backend may have been the one.

* **Return type:**
  [`Renderer`](_autosummary/an.adapters.html.md#an.adapters.Renderer) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### get(name)

The renderer registered as `name`, else the one that claims it
(`get("stage")` is the stage renderer, registered as `cutout`).

* **Return type:**
  [`Renderer`](_autosummary/an.adapters.html.md#an.adapters.Renderer)

#### register_lazy(name, module)

Declare that importing `module` registers the renderer `name`.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* an.adapters.WhiteboardRenderer

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Whiteboard-style renderer (stub).

### an.adapters.get_renderer(name)

Look up a renderer by name in the default registry.

* **Return type:**
  [`Renderer`](_autosummary/an.adapters.html.md#an.adapters.Renderer)

### an.adapters.list_renderers()

Names of all renderers registered in the default registry.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.adapters.register_lazy_renderer(name, module)

Name the module whose import registers renderer `name` (default registry).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.adapters.register_renderer(renderer)

Register a renderer in the default registry.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### Modules

| [`cutout`](_autosummary/an.adapters.cutout.html.md#module-an.adapters.cutout)                     | The cut-out backend's old package: the stage moved to [`an.stage`](_autosummary/an.stage.html.md#module-an.stage) (an#247).   |
|-------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| [`manim_adapter`](_autosummary/an.adapters.manim_adapter.html.md#module-an.adapters.manim_adapter)       | ManimRenderer — a whole-shot renderer for opaque Manim scene files (an#279).                                                                |
| [`remotion_adapter`](_autosummary/an.adapters.remotion_adapter.html.md#module-an.adapters.remotion_adapter) | RemotionRenderer — invoke `npx remotion render` against a generated TSX project.                                                            |
| [`whiteboard`](_autosummary/an.adapters.whiteboard.html.md#module-an.adapters.whiteboard)             | WhiteboardRenderer — stub for hand-drawn / chalkboard-style animation.                                                                      |


# _autosummary/an.adapters.manim_adapter.html.md

# an.adapters.manim_adapter

ManimRenderer — a whole-shot renderer for opaque Manim scene files (an#279).

A Manim shot names a Python scene file and a `Scene` class in it; `an` runs
that file as it is and treats the result like any other rendered shot:

```yaml
## Shot chart (manim)
```yaml shot
options: {source: bar_chart, scene: BarChartStory}
```
```

`options.source` is a key of the project’s `sources` store
(`assets/sources/bar_chart.py`); `options.scene` the class (optional when the
file defines exactly one). The shot has NO IR inside it — it is (a)-level opaque
source on the structured ↔ semantic spectrum — and gets everything around it
from the core: narration (a dialogue line with an off-screen speaker, muxed under
the picture), captions in the sidecar, sound cues, transitions, film assembly,
the shot cache.

**The files a scene reads.** The render runs in a staged copy of the WHOLE
`sources` folder (`assets/sources/` and everything under it), so a scene
imports a sibling module or loads `ImageMobject("bars/logo.png")` by a path
relative to its own file, and every one of those bytes is in both keys below.
A file read from OUTSIDE that folder — a literal absolute path, or one computed
(`Path.home() / "data.csv"`, an environment variable, a helper) — is
RECORDED as the render runs (manimkit’s `record_reads`: an audit hook in the
render’s child process, an#291) and stored with the measurement as a trace
(`read_trace()`): each file’s digest, `absent` for one looked for and
missing, and each listed folder’s names. A stored picture is reused only while
its trace still holds (`stale_reads()`), and the shot key has a `reads`
part, so editing such a file re-renders the picture and the shot, and leaving it
alone reuses both. A literal outside path is still a finding at its line: the
cache sees it now, but another machine will not have it.

**Manim owns its clock** (core study §4.2): only the file’s own `play` and
`wait` calls decide how long it runs. The renderer implements
`measure_duration` (`ClockOwningRenderer`); the
measurement is derived data, kept in the `measurements` store under the
**picture key** — what decides Manim’s own output and length: the bytes of the
sources folder, the entry file, the scene, the Manim and manimkit versions, the
quality preset and, only for a file that uses LaTeX, whether TeX is available.
Never the film’s fps or size, the encode, or this module’s code: those change
how the picture is CONFORMED, not what Manim draws. The core applies it in memory
([`an.measurements`](_autosummary/an.measurements.html.md#module-an.measurements)); the author’s scene is never rewritten.

**The picture is cached apart from the shot.** Manim’s raw video is stored by
its own sha256 (`pictures` store), named by the measurement record under the
picture key, so a change that is not to the picture —
narration, the film’s fps, the background pad, an `an` upgrade — re-conforms
and re-muxes without running Manim. The shot key ([`manim_shot_inputs()`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.manim_shot_inputs))
is the picture key’s inputs plus the conform and encode knobs, the muxed audio
and the render path’s code; the machine is the separate environment part
([`manim_environment()`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.manim_environment)).

**Findings** — manimkit’s layout warnings, lint and errors, a file read the
cache cannot see, a preset whose frame rate the film’s does not divide — are
[`Finding`](_autosummary/an.verify.html.md#an.verify.Finding) s located by `file:line` (`Finding.location`),
stored with the measurement (so a reused shot still reports them) and routed by
the core to `an render`’s warnings, `render_reports/<output>.json`,
`orchestrate` and `an validate`.

**Capabilities** (ADR 0002, environment subject): `env.manim` (`manim` and
`manimkit` importable) is required — absent, [`ManimNotInstalledError`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimNotInstalledError)
with the install command. `env.latex` is required only by a file that uses
LaTeX; absent, the scene renders in manimkit’s `no_latex` mode, so a LaTeX use
fails AT ITS LINE with the remedy instead of deep inside a TeX run.

```pycon
>>> spec = ManimShotSpec.from_options({"source": "chart", "scene": "Chart"})
>>> spec.source, spec.scene, spec.quality
('chart', 'Chart', None)
>>> choose_quality(fps=30, resolution=(1280, 720))
'm'
```

### Module Attributes

| [`DEFAULT_SOURCE_STORE`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.DEFAULT_SOURCE_STORE)   | The project store a shot's `options.source` is a key of.           |
|-------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`CONTACT_SHEET_STORE`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.CONTACT_SHEET_STORE)    | Derived, content-keyed stores (see the module docstring).          |
| [`QUALITY_PRESETS`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.QUALITY_PRESETS)        | Manim Community Edition's presets (`-ql` … `-qk`), smallest first. |

### Functions

| [`choose_quality`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.choose_quality)(\*, fps, resolution)   | The smallest Manim preset that is at least as tall and as fast as the film.                              |
|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| [`manim_environment`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.manim_environment)()                   | The Manim render's machine: Manim's stack, ffmpeg, LaTeX, fonts, the ISA.                                |
| [`manim_shot_inputs`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.manim_shot_inputs)(shot, ctx)          | The Manim renderer's [`ShotKeyer`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyer). |
| [`store_source_resolver`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.store_source_resolver)([store])        | The default resolver: `options.source` is a key of the mall's `store`.                                   |

### Classes

| [`ManimQuality`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimQuality)(letter, width, height, fps)           | One of Manim's quality presets: its letter, pixel size and frame rate.       |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`ManimRenderer`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimRenderer)(\*[, render_check, source_resolver]) | Manim Community Edition, through `manimkit`: an opaque-source shot renderer. |
| [`ManimShotSpec`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimShotSpec)(source[, scene, quality, ...])       | What a Manim shot's `options` say, checked.                                  |
| [`SourceFile`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.SourceFile)(key, entry, closure, display)           | A scene file and its closure, as a resolver read them.                       |

### Exceptions

| [`ManimNotInstalledError`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimNotInstalledError)   | `manim` / `manimkit` are not importable (`env.manim` is absent).   |
|---------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`ManimRenderError`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimRenderError)         | A Manim shot could not be rendered.                                |

### an.adapters.manim_adapter.CONTACT_SHEET_STORE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'contact_sheets'*

Derived, content-keyed stores (see the module docstring).

### an.adapters.manim_adapter.DEFAULT_SOURCE_STORE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'sources'*

The project store a shot’s `options.source` is a key of.

### *exception* an.adapters.manim_adapter.ManimNotInstalledError

Bases: [`ManimRenderError`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimRenderError), [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)

`manim` / `manimkit` are not importable (`env.manim` is absent).

### *class* an.adapters.manim_adapter.ManimQuality(letter, width, height, fps)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One of Manim’s quality presets: its letter, pixel size and frame rate.

### *exception* an.adapters.manim_adapter.ManimRenderError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A Manim shot could not be rendered. Carries the [file:line](file:line) and the fix.

### *class* an.adapters.manim_adapter.ManimRenderer(, render_check=None, source_resolver=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Manim Community Edition, through `manimkit`: an opaque-source shot renderer.

Implements [`Renderer`](_autosummary/an.adapters.html.md#an.adapters.Renderer) and
`ClockOwningRenderer`. Seams: `render_check`
(default `manimkit.render_check()`, imported on first use) and
`source_resolver` (default: the project’s `sources` store,
[`store_source_resolver()`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.store_source_resolver)). The shot cache keys a shot through the
REGISTERED instance’s resolver; a subclass registers its own keyer
(`register_shot_keyer(name, manim_shot_inputs, renderer_type=Sub)`).

#### measure_duration(shot, ctx, , render=True, force=False)

Manim’s length of this shot’s scene, from the `measurements` store,
or rendered now (and stored) when there is none — or when `force`.

* **Return type:**
  `DurationMeasurement` | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### probe_frames(shot, ctx, times)

The film’s frames of `shot` at each of `times`, from its STORED picture (an#347).

Manim owns its clock and renders a whole scene at once, so `an
probe` never runs it: the picture a render stored is conformed
exactly as [`render()`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimRenderer.render) conforms it, and the frames showing at
`times` are read out. With no stored picture it refuses, naming the
render that stores one.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

#### render(shot, ctx)

Render `shot` for exactly `shot.duration` (`an.render` settles it
to the measured length, or longer to hold for its narration).

* **Return type:**
  [`RenderResult`](_autosummary/an.adapters.html.md#an.adapters.RenderResult)

#### stored_reads(key, ctx)

The read trace stored with picture `key` (`None`: none stored,
or not recorded). Reads stores only; never renders.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* an.adapters.manim_adapter.ManimShotSpec(source, scene=None, quality=None, background='#000000', timeout=600.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a Manim shot’s `options` say, checked.

```pycon
>>> ManimShotSpec.from_options({"source": "s", "quality": "z"})
Traceback (most recent call last):
...
an.adapters.manim_adapter.ManimRenderError: options.quality must be one of ['l', 'm', 'h', 'p', 'k'] (Manim's presets), got 'z'
```

### an.adapters.manim_adapter.QUALITY_PRESETS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [ManimQuality](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimQuality)]* *= {'h': ManimQuality(letter='h', width=1920, height=1080, fps=60), 'k': ManimQuality(letter='k', width=3840, height=2160, fps=60), 'l': ManimQuality(letter='l', width=854, height=480, fps=15), 'm': ManimQuality(letter='m', width=1280, height=720, fps=30), 'p': ManimQuality(letter='p', width=2560, height=1440, fps=60)}*

Manim Community Edition’s presets (`-ql` … `-qk`), smallest first.

### *class* an.adapters.manim_adapter.SourceFile(key, entry, closure, display)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A scene file and its closure, as a resolver read them.

`closure` is every file the render stages into its working directory —
`{relative path: bytes}`, the entry file included as `entry` — and
`digest` covers all of it, so a sibling module or an image the scene
loads relatively is in the keys. `display` is how a finding names the
entry (`assets/sources/chart.py`; never an absolute path).

### an.adapters.manim_adapter.choose_quality(, fps, resolution)

The smallest Manim preset that is at least as tall and as fast as the film.

The picture is then resampled and scaled to exactly the film’s rate and size,
so a smaller preset would be upscaled (blur) or frame-doubled (judder).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> choose_quality(fps=15, resolution=(640, 360)), choose_quality(fps=30, resolution=(1920, 1080))
('l', 'h')
>>> choose_quality(fps=120, resolution=(8000, 4000))  # nothing is enough: the largest
'k'
```

### an.adapters.manim_adapter.manim_environment()

The Manim render’s machine: Manim’s stack, ffmpeg, LaTeX, fonts, the ISA.

Python package versions (Manim draws with Cairo and Pango through pycairo
and ManimPango, and writes with PyAV), the full `ffmpeg -version` banner
(the conform and the encode), the LaTeX and dvisvgm builds (a formula’s
glyphs), the installed font set, and the ISA and OS family. Never a path.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.adapters.manim_adapter.manim_shot_inputs(shot, ctx)

The Manim renderer’s [`ShotKeyer`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyer).

Named parts: `source` (every file of the sources folder the render
stages), `manim` (the picture’s other inputs: entry, scene, Manim and
manimkit versions, quality, LaTeX mode — `picture_inputs()`), `knobs`
(fps, size, background and the encode: pixel format, x264 argv, scale
filter, faststart), `audio` (the dialogue muxed under it and the frame
count it is cut to — a held narration moves it), `code`
(`render_code_digest()`) and `reads` (what the files the picture’s
render read outside the sources folder hold NOW —
`current_reads_digest()` of the trace stored with the picture; an#291).
Raises what the render would for a bad shot.

* **Return type:**
  [`ShotKeyInputs`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyInputs)

### an.adapters.manim_adapter.store_source_resolver(store='sources')

The default resolver: `options.source` is a key of the mall’s `store`.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`ManimShotSpec`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.ManimShotSpec), [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`SourceFile`](_autosummary/an.adapters.manim_adapter.html.md#an.adapters.manim_adapter.SourceFile)]

```pycon
>>> resolve = store_source_resolver()
>>> resolve(ManimShotSpec("chart"), {"sources": {"chart": b"x = 1"}}).data
b'x = 1'
```


# _autosummary/an.adapters.remotion_adapter.html.md

# an.adapters.remotion_adapter

RemotionRenderer — invoke `npx remotion render` against a generated TSX project.

Phase 6 skeleton. The full implementation needs a templated Remotion node
project (package.json + Composition.tsx + Root.tsx) and per-shot TSX
generation. For now the renderer raises a clear error when invoked,
documenting what’s needed.

### Classes

| [`RemotionRenderer`](_autosummary/an.adapters.remotion_adapter.html.md#an.adapters.remotion_adapter.RemotionRenderer)()   | Remotion-based renderer (skeleton).   |
|-----------------------------------------------------------------------|---------------------------------------|

### Exceptions

| [`RemotionRenderError`](_autosummary/an.adapters.remotion_adapter.html.md#an.adapters.remotion_adapter.RemotionRenderError)   | Raised when a Remotion render fails.   |
|------------------------------------------------------------------------|----------------------------------------|

### *exception* an.adapters.remotion_adapter.RemotionRenderError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a Remotion render fails.

### *class* an.adapters.remotion_adapter.RemotionRenderer

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Remotion-based renderer (skeleton).


# _autosummary/an.adapters.whiteboard.html.md

# an.adapters.whiteboard

WhiteboardRenderer — stub for hand-drawn / chalkboard-style animation.

Phase 6 ships the registry entry only. The real implementation will likely
be a thin wrapper around the cutout backend with hand-drawn-styled assets
(rough strokes, paper texture), or a Manim subclass with a chalkboard theme.
A spike during v0.1 picks the direction.

### Classes

| [`WhiteboardRenderer`](_autosummary/an.adapters.whiteboard.html.md#an.adapters.whiteboard.WhiteboardRenderer)()   | Whiteboard-style renderer (stub).   |
|-------------------------------------------------------------------------|-------------------------------------|

### Exceptions

| [`WhiteboardRenderError`](_autosummary/an.adapters.whiteboard.html.md#an.adapters.whiteboard.WhiteboardRenderError)   | Raised by the whiteboard stub.   |
|--------------------------------------------------------------------------|----------------------------------|

### *exception* an.adapters.whiteboard.WhiteboardRenderError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised by the whiteboard stub.

### *class* an.adapters.whiteboard.WhiteboardRenderer

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Whiteboard-style renderer (stub).


# _autosummary/an.assemble.html.md

# an.assemble

Film assembly: rendered shots → one film, with transitions and a sound layer.

Every shot renders in isolation (`an.render`); this module decides how the
shots meet and what is heard over them. It runs only when a scene asks for it
— a non-`cut` [`Transition`](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition) or any
[`SoundCue`](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue) — so a scene with neither takes the old
`_ffmpeg_concat` path, byte for byte ([`needs_assembly()`](_autosummary/an.assemble.html.md#an.assemble.needs_assembly)).

**Where each part happens, and why there.**

- *Picture*: transitions are composed in the FRAME STAGE, on the per-shot PNGs,
  in exact integer arithmetic, and encoded by the same
  `an.media.mp4.mux_frames` every shot uses. Composing in ffmpeg (`xfade`)
  would decode already-encoded shots and re-encode them — a second generation
  of x264 loss on every frame of the film, not just the transition — and would
  retire the render pipeline’s “ffmpeg never touches a frame” clause.
- *The picture is a stream-copy concat of SEGMENTS* (an#260), each encoded
  once from PNGs: a shot no transition touches is its own mp4’s video stream,
  copied; a shot a transition touches contributes the encoded span between its
  windows (its *body*) plus the PNGs inside them; each run of composed frames
  is encoded on its own. So a reused shot needs its mp4 (and, at a transition,
  its body and window PNGs, a few dozen frames) — never every frame it has
  ([`shot_windows()`](_autosummary/an.assemble.html.md#an.assemble.shot_windows), [`ShotParts`](_autosummary/an.assemble.html.md#an.assemble.ShotParts)). The film’s frame `i` is at
  `i / fps` and decodes to exactly what its segment decodes to (measured,
  an#260; [`MIN_SEGMENT_FRAMES`](_autosummary/an.assemble.html.md#an.assemble.MIN_SEGMENT_FRAMES) is why no segment is shorter than three).
- *Sound*: the film’s audio is rebuilt from SOURCES — every dialogue line’s
  cached WAV and every cue’s asset, placed in film time — in one ffmpeg mix,
  then muxed onto the picture with `-c:v copy`. Mixing onto the shots’
  already-encoded AAC would be a second audio generation for the dialogue.

**The timeline is frame-exact.** Shot `i` occupies `frame_count(duration,
fps)` frames (the renderer’s own rule) starting at film frame
`FilmTimeline.starts` `[i]`; audio is placed at `start / fps` plus
its shot-local time, so a line stays on the frames it was lip-synced to
whatever the transitions do.

```pycon
>>> from an.ir.schema import Shot, Transition
>>> tl = film_timeline(
...     [Shot(id="a", duration=2.0),
...      Shot(id="b", duration=2.0, transition=Transition(kind="dissolve", duration=0.5))],
...     fps=10,
... )
>>> tl.starts, tl.total_frames   # b starts 5 frames early: the film is 0.5 s shorter
((0, 15), 35)
```

### Module Attributes

| [`MIN_SEGMENT_FRAMES`](_autosummary/an.assemble.html.md#an.assemble.MIN_SEGMENT_FRAMES)   | The fewest frames a segment of a MULTI-segment picture may have.   |
|-----------------------------------------------------------------------|--------------------------------------------------------------------|

### Functions

| [`assemble_film`](_autosummary/an.assemble.html.md#an.assemble.assemble_film)(scene, shot_results, output, ...)    | Assemble rendered shots into `output`: the picture as a concat of segments (transitions composed in), then the mix.                                                             |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`duck_gain`](_autosummary/an.assemble.html.md#an.assemble.duck_gain)(t, spans, \*, duck_db, attack, release)  | The linear gain a ducked cue plays at, at film time `t` — the spec the ffmpeg expressions (`_duck_expressions()`) are written from.                                             |
| [`film_duration`](_autosummary/an.assemble.html.md#an.assemble.film_duration)(scene, \*[, fps])                    | Seconds the delivered film runs: the shots' durations, minus each dissolve's overlap.                                                                                           |
| [`film_timeline`](_autosummary/an.assemble.html.md#an.assemble.film_timeline)(shots, \*, fps[, closing])           | Lay `shots` end to end, overlapping each dissolve, the last fading out by `closing` (the film's closing fade, an#389).                                                          |
| [`needs_assembly`](_autosummary/an.assemble.html.md#an.assemble.needs_assembly)(scene, \*[, fps])                   | True when the scene asks for anything beyond hard cuts and shot audio.                                                                                                          |
| [`picture_segments`](_autosummary/an.assemble.html.md#an.assemble.picture_segments)(timeline, windows)                | The film's picture as segments, in film order.                                                                                                                                  |
| [`shot_parts`](_autosummary/an.assemble.html.md#an.assemble.shot_parts)(frames, window, \*, fps, work_dir)      | A rendered shot's [`ShotParts`](_autosummary/an.assemble.html.md#an.assemble.ShotParts) for `window`, from its frames.                                                                     |
| [`shot_windows`](_autosummary/an.assemble.html.md#an.assemble.shot_windows)(timeline, \*[, min_segment_frames])   | Each shot's [`ShotWindow`](_autosummary/an.assemble.html.md#an.assemble.ShotWindow): the frames its transitions touch, widened until every segment of the picture has `min_segment_frames`. |
| [`transition_problems`](_autosummary/an.assemble.html.md#an.assemble.transition_problems)(shots, fps, \*[, closing])     | Every reason these shots' transitions cannot be assembled, as `(shot index, message)`.                                                                                          |
| [`write_film_frames`](_autosummary/an.assemble.html.md#an.assemble.write_film_frames)(timeline, frame_of, out_dir, \*) | Every frame of the film as a PNG, in `out_dir` — what the delivered picture shows, frame for frame, BEFORE it is encoded.                                                       |

### Classes

| [`FilmTimeline`](_autosummary/an.assemble.html.md#an.assemble.FilmTimeline)(fps, frames, starts, ...[, ...])   | Where each shot's frames land in the film, and what blends them.                                                                                                                                                                                       |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`Segment`](_autosummary/an.assemble.html.md#an.assemble.Segment)(kind, shot, start, stop)                | One independently encoded run of the film's picture, film frames `[start, stop)`: a whole shot's own stream (`"shot"`), a shot's encoded body (`"body"`), or a run of PNGs composed here (`"frames"`).                                                 |
| [`ShotParts`](_autosummary/an.assemble.html.md#an.assemble.ShotParts)(window, frames[, body])               | What a film takes from a shot its transitions touch: the PNGs inside its [`ShotWindow`](_autosummary/an.assemble.html.md#an.assemble.ShotWindow) (`frames`: shot-local index -> path) and its body, encoded once (`body`; `None` when the window covers the shot). |
| [`ShotWindow`](_autosummary/an.assemble.html.md#an.assemble.ShotWindow)(frames[, head, tail])                | Which of one shot's `frames` its film needs as PNGs: the first `head` and the last `tail`.                                                                                                                                                             |

### Exceptions

| [`AssemblyError`](_autosummary/an.assemble.html.md#an.assemble.AssemblyError)   | The shots cannot be assembled as the scene asks.   |
|------------------------------------------------------------------|----------------------------------------------------|

### *exception* an.assemble.AssemblyError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The shots cannot be assembled as the scene asks. Carries the fix.

### *class* an.assemble.FilmTimeline(fps, frames, starts, dissolve_in, fade_in, fade_out, fade_in_color, fade_out_color, total_frames, closing=False, wipe_in=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where each shot’s frames land in the film, and what blends them.

`dissolve_in[i]` — frames shot `i` overlaps the previous shot by.
`fade_in[i]` — frames at shot `i`’s head that fade up from a colour.
`fade_out[i]` — frames at shot `i`’s tail that fade to a colour (the
NEXT shot’s fade colour, or the film’s closing fade’s on the last shot).
`closing` — the last shot’s `fade_out` is the film’s closing fade
(`Meta.closing_transition`, an#389), so its final frame IS the colour:
no next shot reaches it instead.

#### end_seconds(i)

Film time just after shot `i`’s last frame.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

#### start_seconds(i)

Film time of shot `i`’s first frame.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

#### wipe_in *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None), ...]* *= ()*

Per shot, the direction its incoming overlap WIPES in (an#390); `None`
for a dissolve (or no overlap).

### an.assemble.MIN_SEGMENT_FRAMES *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 3*

The fewest frames a segment of a MULTI-segment picture may have. Measured on
ffmpeg 9.0.1 / libx264 with the pinned argv (an#260): a stream of three or
more frames carries a two-frame B-pyramid decode delay (its first DTS is two
frames before its first PTS) whatever its content, and a stream of one or
two frames carries none. The concat demuxer offsets every file alike, so a
delay-free segment between two delayed ones leaves the DTS going backwards;
the muxer patches that with one-tick packets, and a constant-rate decode of
the film then shows a frame twice. [`shot_windows()`](_autosummary/an.assemble.html.md#an.assemble.shot_windows) widens every short
run instead, so each segment of a multi-segment picture has the delay.

### *class* an.assemble.Segment(kind, shot, start, stop)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One independently encoded run of the film’s picture, film frames
`[start, stop)`: a whole shot’s own stream (`"shot"`), a shot’s
encoded body (`"body"`), or a run of PNGs composed here (`"frames"`).

### *class* an.assemble.ShotParts(window, frames, body=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a film takes from a shot its transitions touch: the PNGs inside
its [`ShotWindow`](_autosummary/an.assemble.html.md#an.assemble.ShotWindow) (`frames`: shot-local index -> path) and its
body, encoded once (`body`; `None` when the window covers the shot).

### *class* an.assemble.ShotWindow(frames, head=0, tail=0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Which of one shot’s `frames` its film needs as PNGs: the first
`head` and the last `tail`.

Between them is the shot’s *body*, which the film takes as one encoded
span. A shot with an empty window (`whole`) is taken as its own
mp4’s video stream, so it needs no frame at all.

```pycon
>>> w = ShotWindow(frames=10, head=0, tail=4)
>>> w.whole, w.body, w.png_indices
(False, (0, 6), (6, 7, 8, 9))
```

#### *property* body *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int)]*

the shot-local frames taken as one encoded span.

* **Type:**
  `(first, stop)`

#### *property* png_indices *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), ...]*

The shot-local frames the film needs as PNGs.

### an.assemble.assemble_film(scene, shot_results, output, , fps, mall, work_dir, pix_fmt=None, parts=None)

Assemble rendered shots into `output`: the picture as a concat of
segments (transitions composed in), then the mix.

`shot_results` are the renderers’ `RenderResult`s, in timeline order. A
shot no transition touches contributes its mp4 alone. A shot one touches
needs its :class:`ShotParts` for its [`shot_windows()`](_autosummary/an.assemble.html.md#an.assemble.shot_windows) window: pass them
in `parts` (a reused shot’s come from the shot cache), or the shot’s
`frame_manifest` must hold its frames, from which they are built.

**Why not one mux of every frame**, as before an#260: the picture depended
on every frame of every shot, so a film with a dissolve or a music bed
could reuse no shot without caching all of its PNGs — hundreds of MB per
1080p shot. The segment concat puts frame `i` at `i / fps` exactly as
the one mux did (measured, an#260), and each frame decodes to exactly what
its own segment decodes to.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.assemble.duck_gain(t, spans, , duck_db, attack, release)

The linear gain a ducked cue plays at, at film time `t` — the spec the
ffmpeg expressions (`_duck_expressions()`) are written from.

Full level away from dialogue; `duck_db` down while a line plays; a linear
ramp over `attack` seconds BEFORE each line (so its first syllable is
already clear) and `release` seconds after.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> spans = [(1.0, 2.0)]
>>> [round(duck_gain(t, spans, duck_db=-20, attack=0.5, release=0.5), 3)
...  for t in (0.0, 0.75, 1.5, 2.25, 3.0)]
[1.0, 0.55, 0.1, 0.55, 1.0]
```

### an.assemble.film_duration(scene, , fps=None)

Seconds the delivered film runs: the shots’ durations, minus each
dissolve’s overlap. Exactly `sum(durations)` for a scene without one, so
every existing document’s arithmetic is unchanged.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> from an.ir.schema import SceneIR, Shot, Transition
>>> film_duration(SceneIR(timeline=[Shot(id="a", duration=2.0),
...     Shot(id="b", duration=2.0, transition=Transition(kind="dissolve", duration=0.5))]))
3.5
```

### an.assemble.film_timeline(shots, , fps, closing=None)

Lay `shots` end to end, overlapping each dissolve, the last fading out
by `closing` (the film’s closing fade, an#389). Raises
[`AssemblyError`](_autosummary/an.assemble.html.md#an.assemble.AssemblyError) on any [`transition_problems()`](_autosummary/an.assemble.html.md#an.assemble.transition_problems).

* **Return type:**
  [`FilmTimeline`](_autosummary/an.assemble.html.md#an.assemble.FilmTimeline)

### an.assemble.needs_assembly(scene, , fps=None)

True when the scene asks for anything beyond hard cuts and shot audio.

Decided on FRAMES at the render’s rate (`fps`, default the scene’s): a
transition that rounds to zero frames asks for nothing, and must not cost a
scene its byte-identical concat.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> from an.ir.schema import Meta, SceneIR, Shot, Transition
>>> needs_assembly(SceneIR(timeline=[Shot(id="a"), Shot(id="b")]))
False
>>> needs_assembly(SceneIR(timeline=[
...     Shot(id="a"), Shot(id="b", transition=Transition(kind="fade", duration=0.0))]))
False
```

### an.assemble.picture_segments(timeline, windows)

The film’s picture as segments, in film order.

A film frame is part of a shot’s body when exactly one shot shows it and
that frame is outside the shot’s window; every other frame (a blend, a
fade, or a frame a window was widened over) is composed from PNGs.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Segment`](_autosummary/an.assemble.html.md#an.assemble.Segment)]

```pycon
>>> from an.ir.schema import Shot, Transition
>>> tl = film_timeline([Shot(id="a", duration=1.0), Shot(id="b", duration=1.0,
...     transition=Transition(kind="dissolve", duration=0.4))], fps=10)
>>> [(s.kind, s.shot, s.start, s.stop) for s in picture_segments(tl, shot_windows(tl))]
[('body', 0, 0, 6), ('frames', None, 6, 10), ('body', 1, 10, 16)]
```

### an.assemble.shot_parts(frames, window, , fps, work_dir, pix_fmt=None)

A rendered shot’s [`ShotParts`](_autosummary/an.assemble.html.md#an.assemble.ShotParts) for `window`, from its frames.

The body is encoded by `an.media.mp4.mux_frames` — the shot mux’s own
encoder and argv — from the body’s PNGs, renumbered from zero in
`work_dir`. The window’s PNGs are referenced where they are.

* **Return type:**
  [`ShotParts`](_autosummary/an.assemble.html.md#an.assemble.ShotParts)

### an.assemble.shot_windows(timeline, , min_segment_frames=3)

Each shot’s [`ShotWindow`](_autosummary/an.assemble.html.md#an.assemble.ShotWindow): the frames its transitions touch,
widened until every segment of the picture has `min_segment_frames`.

A pure function of the timeline, so the render loop, the shot cache and
the garbage collector all agree on what a shot’s film needs from it. A
picture of one segment has no minimum (there is nothing to concatenate).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`ShotWindow`](_autosummary/an.assemble.html.md#an.assemble.ShotWindow), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> from an.ir.schema import Shot, Transition
>>> d = Transition(kind="dissolve", duration=0.1)   # one frame at 10 fps
>>> tl = film_timeline([Shot(id="a", duration=1.0), Shot(id="b", duration=1.0,
...     transition=d)], fps=10)
>>> [(w.head, w.tail) for w in shot_windows(tl)]   # the 1-frame run, widened to 3
[(0, 1), (3, 0)]
```

### an.assemble.transition_problems(shots, fps, , closing=None)

Every reason these shots’ transitions cannot be assembled, as
`(shot index, message)`. The ONE list `an validate` reports and
[`film_timeline()`](_autosummary/an.assemble.html.md#an.assemble.film_timeline) raises on, so the two cannot disagree.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> from an.ir.schema import Shot, Transition
>>> transition_problems([Shot(id="a", transition=Transition(kind="dissolve"))], fps=30)
[(0, "shot 'a' is the first shot, so a dissolve has nothing to dissolve from; use a fade (from a colour) or a cut")]
```

### an.assemble.write_film_frames(timeline, frame_of, out_dir, , pattern=None)

Every frame of the film as a PNG, in `out_dir` — what the delivered
picture shows, frame for frame, BEFORE it is encoded.

The film itself is a concat of segments (an#260) and never holds this
sequence on disk; a measurement that needs the composed picture — the
bench’s reference for an assembled scene (an#279) — builds it here, with
the same per-frame composition (`_compose_frame()`) the segments use.
`frame_of(i, j)` is the PNG of frame `j` of shot `i`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]


# _autosummary/an.audio.cli.html.md

# an.audio.cli

`an voices ...` — browse a TTS provider’s voices from the shell (an#209).

Thin string-typed wrappers dispatched by typer the way `an character ...` is;
the business logic is [`browse_voices()`](_autosummary/an.audio.cli.html.md#an.audio.cli.browse_voices), a plain function over the
provider factories.

> an voices list –provider elevenlabs
> an voices list –provider elevenlabs –search british
> an voices list –provider mac_say
> an voices rescore <project> “He did not ask”    # re-choose a line’s take from its cached takes
> an voices reroll <project> “He did not ask”     # synthesize new takes for it (billed)

The `voice_id` column is what a `voices`-store document’s `voice_id` takes.
`rescore` and `reroll` are the only ways a recorded best-of-N take is
replaced ([`an.audio.pipeline.retake_lines()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.retake_lines)); the next `an render` does
the choosing, and prints what it will bill first.

### Functions

| [`browse_voices`](_autosummary/an.audio.cli.html.md#an.audio.cli.browse_voices)([provider, search, make])         | The voices `provider` exposes, optionally filtered by `search`.         |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`format_voices`](_autosummary/an.audio.cli.html.md#an.audio.cli.format_voices)(voices)                           | One line per voice: `voice_id  name  (labels)`.                         |
| [`retake`](_autosummary/an.audio.cli.html.md#an.audio.cli.retake)(project, line, \*[, rescore, tts, make]) | Release the recorded takes of the lines of `project` containing `line`. |

### an.audio.cli.browse_voices(provider='elevenlabs', \*, search=None, make=<function make_tts>)

The voices `provider` exposes, optionally filtered by `search`.

A provider whose `list_voices` takes `search` (ElevenLabs) filters on
its server; for the others the filter is a case-insensitive substring over
the name, id and labels.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`VoiceMeta`](_autosummary/an.audio.tts.html.md#an.audio.tts.VoiceMeta)]

```pycon
>>> from an.audio.tts import VoiceMeta
>>> class Fake:
...     name = "fake"
...     def list_voices(self):
...         return [VoiceMeta("v1", "Ada", "fake"), VoiceMeta("v2", "Bob", "fake")]
>>> [v.name for v in browse_voices("fake", search="ad", make=lambda _: Fake())]
['Ada']
```

### an.audio.cli.format_voices(voices)

One line per voice: `voice_id  name  (labels)`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from an.audio.tts import VoiceMeta
>>> print(format_voices([VoiceMeta("abc", "Ada", "x", extra={"labels": {"accent": "british"}})]))
abc  Ada  (accent=british)
```

### an.audio.cli.retake(project, line, \*, rescore=False, tts='elevenlabs', make=<function make_tts>)

Release the recorded takes of the lines of `project` containing `line`.

The plain-function core of `an voices rescore` / `an voices reroll`: see
[`an.audio.pipeline.retake_lines()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.retake_lines). `tts` must be the provider the
line is rendered with (its takes are keyed by it).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.audio.effects.html.md

# an.audio.effects

Voice effects: a deterministic transform applied to a synthesized line (an#163).

A voice document in the `voices` store may declare `effects`. Three exist:

- `pitch_semitones` (an#163) — South Park raises its voices to sound like
  fourth graders; duration-preserving.
- `tempo` (an#265) — a pitch-preserving speed ratio (`1.1` is 10% faster).
  `eleven_v3` ignores `voice_settings.speed` and no audio tag moves its rate,
  so a narrator that must run at 5.8 syllables/s, or an expressive voice that
  must be slowed to a serene pace, is re-timed here. Unlike the pitch effect it
  CHANGES the line’s duration.
- `trim_silence` (an#254) — cut the silence before the first word and after
  the last, keeping a little of each (`true`, or `{threshold_db, keep_lead_s,
  keep_tail_s}`). A real voice pads a line: `eleven_v3` returned 1.6 s for
  “Hi!” — 0.4 s of breath before the word and 0.65 s of room tone after it — so
  the line started late and the next one ran past its shot. It CHANGES the
  line’s duration too, and it is OPT-IN: a voice that values its breaths (a
  style that keeps a performance’s natural artifacts) declares nothing, or a
  lower threshold, and keeps them.

A raised voice or a fast narrator is a property of the *character*, not of the
TTS provider, so it lives beside the voice the line already resolves through
rather than in the IR.

```pycon
>>> normalize_effects({"pitch_semitones": 4})
{'pitch_semitones': 4.0, 'chain_version': 2}
>>> normalize_effects({"pitch_semitones": 0}) == normalize_effects({"tempo": 1}) == normalize_effects(None) == {}
True
>>> normalize_effects({"tempo": 1.1, "pitch_semitones": -2})
{'pitch_semitones': -2.0, 'tempo': 1.1, 'chain_version': 2}
>>> round(_pitch_filter(12), 3)
0.5
>>> filter_chain({"tempo": 1.25})
'aresample=44100,apad=pad_dur=0.25,atempo=1.250000000'
>>> normalize_effects({"trim_silence": True})["trim_silence"]
{'keep_lead_s': 0.1, 'keep_tail_s': 0.2, 'threshold_db': -20.0, 'version': 1}
>>> normalize_effects({"trim_silence": False}) == {}
True
>>> normalize_effects({"reverb": 1})
Traceback (most recent call last):
    ...
an.audio.effects.VoiceEffectError: unknown voice effect(s) ['reverb']; known: ['pitch_semitones', 'tempo', 'trim_silence']
```

Design, in the order the pipeline uses it:

- **The transform runs after synthesis and before alignment.** Lip-sync reads the
  audio the viewer hears. The pitch chain keeps the duration, so word timings
  computed on raw and on shifted audio agree to a frame; a `tempo` does not,
  which is why the shifted bytes are what is aligned, the line’s `duration` is
  read from them, and the viseme key derives from the effect-keyed audio key —
  visemes, word timings and captions all follow the audio the viewer hears.
- **The cache keys on the effect.** `effects_key` is empty for no effect, and
  `an.audio.pipeline` adds it to the audio key only when it is non-empty, so a
  project that declares none keeps every key it ever had.
- **Stock ffmpeg only.** The chain is `aresample → asetrate → aresample →
  atempo`: the sample rate is relabelled by the pitch ratio (pitch and speed both
  move) and `atempo` removes the speed change; a `tempo` multiplies into that
  same `atempo` factor (`aresample → atempo` alone when there is no pitch).
  A factor outside `atempo`’s clean `[0.5, 2]` is split into stages, each
  inside it; the pitch-only chain is one stage, character for character what it
  was before `tempo` existed. `rubberband` is a better
  shifter but is a build option, and its output would differ between machines —
  which a content-hash cache cannot tolerate. The output is bit-exact WAV (no
  encoder tag, no metadata), so two runs produce identical bytes.
- **The trim runs last, in Python**, on the WAV the chain wrote (or on the
  synthesized audio, decoded by ffmpeg only when it is not 16-bit PCM WAV —
  ElevenLabs sends MP3): the level of each [`TRIM_WINDOW_S`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_WINDOW_S) window is
  compared with the line’s loudest, so a quiet voice and a loud one are cut
  alike, and the kept padding is in the seconds the viewer hears. Its
  parameters, resolved (defaults included) with [`TRIM_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_VERSION), are what
  the key holds, so a changed default re-trims instead of replaying. What it
  cut is RECORDED in the WAV it writes — a standard `LIST`/`INFO` comment
  after the samples, which every reader skips — and [`trim_record()`](_autosummary/an.audio.effects.html.md#an.audio.effects.trim_record) reads
  it back: the record travels with the bytes it describes and is collected
  with them. A line with nothing above the threshold (the offline voice’s
  silence) is left whole.

### Module Attributes

| [`PITCH_SEMITONES_LIMIT`](_autosummary/an.audio.effects.html.md#an.audio.effects.PITCH_SEMITONES_LIMIT)   | Effects a voice document may declare, with the range each accepts.                                                                                                                                                                                        |
|--------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`TEMPO_LIMITS`](_autosummary/an.audio.effects.html.md#an.audio.effects.TEMPO_LIMITS)            | half to double speed.                                                                                                                                                                                                                                     |
| [`ATEMPO_STAGE_LIMITS`](_autosummary/an.audio.effects.html.md#an.audio.effects.ATEMPO_STAGE_LIMITS)     | One `atempo` stage's clean range; a factor beyond it is chained in stages.                                                                                                                                                                                |
| [`EFFECT_SAMPLE_RATE`](_autosummary/an.audio.effects.html.md#an.audio.effects.EFFECT_SAMPLE_RATE)      | The sample rate the chain runs at (and the shifted WAV is written at).                                                                                                                                                                                    |
| [`TRIM_SILENCE`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_SILENCE)            | The effect that cuts a line's leading and trailing silence (an#254).                                                                                                                                                                                      |
| [`DFLT_TRIM_THRESHOLD_DB`](_autosummary/an.audio.effects.html.md#an.audio.effects.DFLT_TRIM_THRESHOLD_DB)  | `trim_silence`'s defaults.                                                                                                                                                                                                                                |
| [`DFLT_TRIM_KEEP_LEAD_S`](_autosummary/an.audio.effects.html.md#an.audio.effects.DFLT_TRIM_KEEP_LEAD_S)   | more than the lip-sync anticipation lead (2/24 s), so the mouth can open before the sound.                                                                                                                                                                |
| [`DFLT_TRIM_KEEP_TAIL_S`](_autosummary/an.audio.effects.html.md#an.audio.effects.DFLT_TRIM_KEEP_TAIL_S)   | a word's release and decay.                                                                                                                                                                                                                               |
| [`TRIM_THRESHOLD_LIMITS`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_THRESHOLD_LIMITS)   | `threshold_db` bounds (relative to the line's loudest window).                                                                                                                                                                                            |
| [`TRIM_KEEP_LIMITS`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_KEEP_LIMITS)        | `keep_lead_s` / `keep_tail_s` bounds, seconds.                                                                                                                                                                                                            |
| [`TRIM_WINDOW_S`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_WINDOW_S)           | a change to it (or to anything else that moves a trimmed line's bytes) is a [`TRIM_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_VERSION) bump — a test pins the trim's output to the version (an#309).                                                   |
| [`TRIM_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_VERSION)            | Bumped whenever a trimmed line's bytes would change (algorithm, window, record tag, rounding); part of every trimmed line's key.                                                                                                                          |
| [`CHAIN_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.CHAIN_VERSION)           | The version of the ffmpeg chain every effected line passes (pitch, tempo, and the decode of a non-WAV line before its trim): its filters, rate ([`EFFECT_SAMPLE_RATE`](_autosummary/an.audio.effects.html.md#an.audio.effects.EFFECT_SAMPLE_RATE)), `atempo` stages and flags (an#309). |
| [`TEMPO_PAD_S`](_autosummary/an.audio.effects.html.md#an.audio.effects.TEMPO_PAD_S)             | Seconds of silence appended before the `atempo` stages, so their window never eats the line's tail (it dropped 20-40 ms of every short padded clip, an#350); the output is then cut to the exact length the tempo gives.                                  |
| [`TRIM_RECORD_TAG`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_RECORD_TAG)         | The prefix of the `LIST`/`INFO` comment a trimmed WAV carries.                                                                                                                                                                                            |

### Functions

| [`apply_voice_effects`](_autosummary/an.audio.effects.html.md#an.audio.effects.apply_voice_effects)(audio, effects)             | `audio` (any container ffmpeg sniffs) with `effects` applied, as WAV bytes.                                                                                            |
|--------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`atempo_stages`](_autosummary/an.audio.effects.html.md#an.audio.effects.atempo_stages)(factor, \*[, limits])             | `factor` as a product of `atempo` stages, each inside `limits`.                                                                                                        |
| [`decode_chain`](_autosummary/an.audio.effects.html.md#an.audio.effects.decode_chain)()                                  | The chain that decodes a non-WAV line (MP3, from ElevenLabs) before its trim (part of [`CHAIN_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.CHAIN_VERSION)).                 |
| [`ffmpeg_argv`](_autosummary/an.audio.effects.html.md#an.audio.effects.ffmpeg_argv)(chain, out_path, \*[, source_path]) | The argv that runs `chain` over audio on stdin into a bit-exact 16-bit PCM WAV at `out_path` (part of [`CHAIN_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.CHAIN_VERSION)). |
| [`filter_chain`](_autosummary/an.audio.effects.html.md#an.audio.effects.filter_chain)(effects)                           | The ffmpeg `-af` chain for normalised `effects` (`""` for none).                                                                                                       |
| [`normalize_effects`](_autosummary/an.audio.effects.html.md#an.audio.effects.normalize_effects)(raw)                          | The canonical effects dict for a voice's `effects` value.                                                                                                              |
| [`speech_end`](_autosummary/an.audio.effects.html.md#an.audio.effects.speech_end)(audio, \*[, threshold_db, window_s]) | Seconds into `audio` at which its audible speech ends (an#397); `None` when silent or unreadable.                                                                      |
| [`trim_record`](_autosummary/an.audio.effects.html.md#an.audio.effects.trim_record)(wav)                                | What `trim_silence` cut from `wav` (`lead_s`, `tail_s`, `source_s`), read from the comment it wrote; `None` for audio it did not write.                                |
| [`trim_silence`](_autosummary/an.audio.effects.html.md#an.audio.effects.trim_silence)(wav, \*[, threshold_db, ...])      | `wav` (16-bit PCM) cut to its speech, plus `keep_lead_s` before it and `keep_tail_s` after it, and the cut recorded in the WAV it returns.                             |
| [`voice_effects`](_autosummary/an.audio.effects.html.md#an.audio.effects.voice_effects)(mall, voice_id)                   | The normalised effects declared by `mall["voices"][voice_id]`, or `{}`.                                                                                                |

### Classes

| [`SilenceTrim`](_autosummary/an.audio.effects.html.md#an.audio.effects.SilenceTrim)(audio, lead_s, tail_s, source_s)   | What [`trim_silence()`](_autosummary/an.audio.effects.html.md#an.audio.effects.trim_silence) produced: the trimmed WAV, and what it cut.   |
|-------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|

### Exceptions

| [`VoiceEffectError`](_autosummary/an.audio.effects.html.md#an.audio.effects.VoiceEffectError)   | A voice declares an effect that is unknown, malformed or out of range.   |
|---------------------------------------------------------------------|--------------------------------------------------------------------------|

### an.audio.effects.ATEMPO_STAGE_LIMITS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= (0.5, 2.0)*

One `atempo` stage’s clean range; a factor beyond it is chained in stages.

### an.audio.effects.CHAIN_VERSION *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 2*

The version of the ffmpeg chain every effected line passes (pitch, tempo,
and the decode of a non-WAV line before its trim): its filters, rate
([`EFFECT_SAMPLE_RATE`](_autosummary/an.audio.effects.html.md#an.audio.effects.EFFECT_SAMPLE_RATE)), `atempo` stages and flags (an#309). Keyed
only once it is not the first, so introducing it moved no key; bump it with
any change to [`filter_chain()`](_autosummary/an.audio.effects.html.md#an.audio.effects.filter_chain), [`ffmpeg_argv()`](_autosummary/an.audio.effects.html.md#an.audio.effects.ffmpeg_argv) or their constants
— a test pins them to the version. Moving the key re-processes each line
from its raw take, which is cached: nothing is billed.

2 (an#350): silence padded before the `atempo` stages and the output cut
to exactly `input / tempo` — `atempo` dropped the last 20-40 ms.

### an.audio.effects.DFLT_TRIM_KEEP_LEAD_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.1*

more than the lip-sync
anticipation lead (2/24 s), so the mouth can open before the sound.

* **Type:**
  Kept before the first window above the threshold

### an.audio.effects.DFLT_TRIM_KEEP_TAIL_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.2*

a word’s release and decay.

* **Type:**
  Kept after the last window above it

### an.audio.effects.DFLT_TRIM_THRESHOLD_DB *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= -20.0*

`trim_silence`’s defaults. A window is “speech” when its RMS level is within
`threshold_db` of the line’s loudest window. Measured on two eleven_v3 lines
(an#254): the breath before “Hi!” peaks 25 dB under the word and its room
tone 35-40 dB under, while each word’s own edges stay within 20 dB — so -20
cuts the breath and the tail and keeps the word. Lower it (-45) to keep breaths.

### an.audio.effects.EFFECT_SAMPLE_RATE *= 44100*

The sample rate the chain runs at (and the shifted WAV is written at).

### an.audio.effects.PITCH_SEMITONES_LIMIT *= 12.0*

Effects a voice document may declare, with the range each accepts.

### *class* an.audio.effects.SilenceTrim(audio, lead_s, tail_s, source_s)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What [`trim_silence()`](_autosummary/an.audio.effects.html.md#an.audio.effects.trim_silence) produced: the trimmed WAV, and what it cut.

#### lead_s *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Seconds cut before the kept audio, and after it.

#### record()

The record the trimmed WAV carries ([`trim_record()`](_autosummary/an.audio.effects.html.md#an.audio.effects.trim_record)).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

#### source_s *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The length of the audio before the trim, seconds.

### an.audio.effects.TEMPO_LIMITS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= (0.5, 2.0)*

half to double speed. Outside it speech stops being speech.

* **Type:**
  `tempo` bounds

### an.audio.effects.TEMPO_PAD_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.25*

Seconds of silence appended before the `atempo` stages, so their window
never eats the line’s tail (it dropped 20-40 ms of every short padded clip,
an#350); the output is then cut to the exact length the tempo gives.
Enough for the slowest stage of [`TEMPO_LIMITS`](_autosummary/an.audio.effects.html.md#an.audio.effects.TEMPO_LIMITS) at every pitch.

### an.audio.effects.TRIM_KEEP_LIMITS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= (0.0, 2.0)*

`keep_lead_s` / `keep_tail_s` bounds, seconds.

### an.audio.effects.TRIM_RECORD_TAG *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an:trim_silence '*

The prefix of the `LIST`/`INFO` comment a trimmed WAV carries.

### an.audio.effects.TRIM_SILENCE *= 'trim_silence'*

The effect that cuts a line’s leading and trailing silence (an#254).

### an.audio.effects.TRIM_THRESHOLD_LIMITS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= (-80.0, -3.0)*

`threshold_db` bounds (relative to the line’s loudest window).

### an.audio.effects.TRIM_VERSION *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 1*

Bumped whenever a trimmed line’s bytes would change (algorithm, window,
record tag, rounding); part of every trimmed line’s key.

### an.audio.effects.TRIM_WINDOW_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.02*

a change to it (or
to anything else that moves a trimmed line’s bytes) is a [`TRIM_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.TRIM_VERSION)
bump — a test pins the trim’s output to the version (an#309).

* **Type:**
  The level-measuring window, seconds. Not keyed itself

### *exception* an.audio.effects.VoiceEffectError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A voice declares an effect that is unknown, malformed or out of range.

### an.audio.effects.apply_voice_effects(audio, effects)

`audio` (any container ffmpeg sniffs) with `effects` applied, as WAV bytes.

Returns the input unchanged for no effects. Raises `VoiceEffectError` when
ffmpeg is missing or fails — never returns unprocessed audio for a voice that
asked for an effect. `trim_silence` alone on a 16-bit PCM WAV needs no
ffmpeg ([`trim_silence()`](_autosummary/an.audio.effects.html.md#an.audio.effects.trim_silence)).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### an.audio.effects.atempo_stages(factor, , limits=(0.5, 2.0))

`factor` as a product of `atempo` stages, each inside `limits`.

One stage when `factor` already fits (the pitch-only chain, always).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> atempo_stages(1.5)
[1.5]
>>> atempo_stages(3.0)
[2.0, 1.5]
>>> atempo_stages(0.3)
[0.5, 0.6]
```

### an.audio.effects.decode_chain()

The chain that decodes a non-WAV line (MP3, from ElevenLabs) before its
trim (part of [`CHAIN_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.CHAIN_VERSION)).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.audio.effects.ffmpeg_argv(chain, out_path, , source_path=None)

The argv that runs `chain` over audio on stdin into a bit-exact 16-bit
PCM WAV at `out_path` (part of [`CHAIN_VERSION`](_autosummary/an.audio.effects.html.md#an.audio.effects.CHAIN_VERSION)).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

source_path: also write the input, decoded ([`decode_chain()`](_autosummary/an.audio.effects.html.md#an.audio.effects.decode_chain)) and
: untouched, there — in the same run, so its length is known whatever
  container came in (an#350: a tempo chain’s output is cut to it)

```pycon
>>> ffmpeg_argv("aresample=44100", "o.wav")[-3:]
['-c:a', 'pcm_s16le', 'o.wav']
>>> ffmpeg_argv("atempo=2", "o.wav", source_path="s.wav")[-1]
's.wav'
```

### an.audio.effects.filter_chain(effects)

The ffmpeg `-af` chain for normalised `effects` (`""` for none).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.audio.effects.normalize_effects(raw)

The canonical effects dict for a voice’s `effects` value.

Omit-when-unset: `None`, `{}` and a zero-valued effect all normalise to
`{}`, which the pipeline treats as “no effect” (and keys nothing on).
Unknown keys raise — an effect that silently does nothing is worse than none.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.audio.effects.speech_end(audio, , threshold_db=-20.0, window_s=0.02)

Seconds into `audio` at which its audible speech ends (an#397); `None` when silent or unreadable.

The end of the last `window_s` window whose RMS level is within
`threshold_db` of the loudest window’s — [`trim_silence()`](_autosummary/an.audio.effects.html.md#an.audio.effects.trim_silence)’s rule, so
a take’s own trailing silence (a provider pads ~0.3 s) is not counted. A
16-bit PCM WAV is read directly; any other container is decoded with ffmpeg
when it is installed.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> import array, io, wave
>>> rate = 8000
>>> samples = array.array("h", [9000, -9000] * (rate // 4) + [0] * rate)  # 0.5 s, then 1 s silent
>>> buf = io.BytesIO()
>>> with wave.open(buf, "wb") as w:
...     w.setnchannels(1); w.setsampwidth(2); w.setframerate(rate); w.writeframes(samples.tobytes())
>>> round(speech_end(buf.getvalue()), 2)
0.5
```

### an.audio.effects.trim_record(wav)

What `trim_silence` cut from `wav` (`lead_s`, `tail_s`, `source_s`),
read from the comment it wrote; `None` for audio it did not write.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.audio.effects.trim_silence(wav, , threshold_db=-20.0, keep_lead_s=0.1, keep_tail_s=0.2, window_s=0.02)

`wav` (16-bit PCM) cut to its speech, plus `keep_lead_s` before it and
`keep_tail_s` after it, and the cut recorded in the WAV it returns.

Speech is every `window_s` window whose RMS level is within
`threshold_db` of the loudest window’s; only the edges move, never a pause
between words. A clip with no sound at all is returned whole (with the
record of a zero cut), and a pad is never longer than the silence it keeps.

* **Return type:**
  [`SilenceTrim`](_autosummary/an.audio.effects.html.md#an.audio.effects.SilenceTrim)

```pycon
>>> import array, io, math, wave
>>> rate = 8000
>>> tone = [int(9000 * math.sin(i / 3)) for i in range(rate // 2)]
>>> samples = array.array("h", [0] * rate + tone + [0] * rate)   # 1 s, 0.5 s, 1 s
>>> buf = io.BytesIO()
>>> with wave.open(buf, "wb") as w:
...     w.setnchannels(1); w.setsampwidth(2); w.setframerate(rate)
...     w.writeframes(samples.tobytes())
>>> cut = trim_silence(buf.getvalue(), keep_lead_s=0.1, keep_tail_s=0.2)
>>> round(cut.lead_s, 2), round(cut.tail_s, 2), round(cut.source_s, 2)
(0.9, 0.8, 2.5)
>>> trim_record(cut.audio) == cut.record()
True
>>> with wave.open(io.BytesIO(cut.audio)) as w:
...     round(w.getnframes() / w.getframerate(), 2)
0.8
```

### an.audio.effects.voice_effects(mall, voice_id)

The normalised effects declared by `mall["voices"][voice_id]`, or `{}`.

A voice that is not in the store (the offline default, a raw provider voice
id) has no effects; so does a store that does not exist.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]


# _autosummary/an.audio.elevenlabs_tts.html.md

# an.audio.elevenlabs_tts

ElevenLabsTTS — real speech via the ElevenLabs API. Requires ELEVEN_API_KEY.

Lazily imports the elevenlabs SDK so the rest of `an` works without it.
If you want real speech, `pip install elevenlabs` and set
`ELEVEN_API_KEY` (or `ELEVENLABS_API_KEY`) in your environment.

**Expressive voices (an#209).** A voice document in the `voices` store may
declare, beside `voice_id`, the `model_id` it speaks with, its
`voice_settings` and a sampling `seed`; a dialogue line’s `[emotion]` and
`{direction}` reach a model that takes inline audio tags (`eleven_v3`,
`eleven_v4` and their variants) as `[excited] Hi!`.
[`ElevenLabsTTS.synthesis_options()`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.ElevenLabsTTS.synthesis_options) turns those into the keyword arguments
[`ElevenLabsTTS.synthesize()`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.ElevenLabsTTS.synthesize) takes — the audio pipeline keys its cache on
exactly that dict, and a voice declaring none of them yields `{}`, so no
existing cache key moves.

```pycon
>>> tts = ElevenLabsTTS(api_key="unused")
>>> tts.synthesis_options({})
{}
>>> tts.synthesis_options({"model_id": "eleven_v3"}, emotion="happy", direction=["sighs"])
{'model_id': 'eleven_v3', 'audio_tags': ['happy', 'sighs']}
>>> tts.synthesis_options({"voice_settings": {"stability": 0.3, "speed": 1.1}})
{'voice_settings': {'speed': 1.1, 'stability': 0.3}}
>>> tagged_text("Hi!", ["excited"])
'[excited] Hi!'
```

### Module Attributes

| [`AUDIO_TAG_MODEL_PREFIXES`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.AUDIO_TAG_MODEL_PREFIXES)   | Model ids that read inline audio tags (`[excited]`, `[sighs]`).                            |
|-----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| [`VOICE_SETTINGS_RANGES`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.VOICE_SETTINGS_RANGES)      | The `voice_settings` keys the API takes, with the range each accepts (`None` = a boolean). |

### Functions

| [`normalize_voice_settings`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.normalize_voice_settings)(raw)              | The canonical `voice_settings` dict — only what was declared, key-sorted.   |
|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`tagged_text`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.tagged_text)(text, tags)                    | `text` with each tag prefixed as `[tag]` — what an audio-tag model reads.   |
| [`takes_audio_tags`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.takes_audio_tags)(model_id, \*[, prefixes]) | Whether `model_id` reads inline audio tags.                                 |

### Classes

| [`ElevenLabsTTS`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.ElevenLabsTTS)(\*[, api_key, model_id, ...])   | ElevenLabs-backed TTSProvider.   |
|------------------------------------------------------------------------------------------------|----------------------------------|

### Exceptions

| [`ElevenLabsUnavailableError`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.ElevenLabsUnavailableError)   | A request cannot be sent: no API key is configured.               |
|-------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`ElevenLabsVoiceError`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.ElevenLabsVoiceError)         | A voice document declares ElevenLabs settings that are malformed. |

### an.audio.elevenlabs_tts.AUDIO_TAG_MODEL_PREFIXES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('eleven_v3', 'eleven_v4')*

Model ids that read inline audio tags (`[excited]`, `[sighs]`). Matched
as prefixes, so `eleven_v3_conversational` and `eleven_v4_turbo` count.
Every other model would speak the brackets, so it never receives a tag.

### *class* an.audio.elevenlabs_tts.ElevenLabsTTS(, api_key=None, model_id='eleven_turbo_v2_5', output_format='mp3_44100_128', audio_tag_model_prefixes=('eleven_v3', 'eleven_v4'), client_factory=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

ElevenLabs-backed TTSProvider. Constructor takes an optional api_key
(falls back to `ELEVEN_API_KEY` / `ELEVENLABS_API_KEY`).

Implements the `TTSProvider` protocol, plus the optional
`synthesis_options` hook the audio pipeline reads (an#209).

#### billed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

what may never run in memory).

* **Type:**
  Every request is charged (an#311

#### billed_characters(text, , audio_tags=None, \*\*\_options)

Characters one request bills: the text as sent, audio tags included.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> ElevenLabsTTS(api_key="unused").billed_characters("Hi!", audio_tags=["excited"])
13
```

#### check_available()

Raise [`ElevenLabsUnavailableError`](_autosummary/an.audio.elevenlabs_tts.html.md#an.audio.elevenlabs_tts.ElevenLabsUnavailableError) when a request could not be
sent (no API key) — what the audio pipeline asks before the first
request of a render, so nothing is half-synthesized (an#305).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### client_factory

`api_key -> client`; tests inject a fake so nothing reaches the API.

#### list_voices(, search=None)

The account’s voices (its own plus the ones it saved), newest API.

`search` filters by name, description and labels on the server. An
absent key or SDK yields `[]`; a key that is present and a call that
fails RAISES — an empty listing must mean “no voices”, not “it broke”.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`VoiceMeta`](_autosummary/an.audio.tts.html.md#an.audio.tts.VoiceMeta)]

#### synthesis_options(voice, , emotion=None, direction=None)

The `synthesize` keyword arguments a voice document and a line imply.

Only what is declared appears (`{}` for a plain voice), so the audio
cache key — which includes this dict when it is non-empty — is unchanged
for every voice that declares nothing new. `audio_tags` are the line’s
`[emotion]` (unless `neutral`) then its `direction` cues, and only
on a model that reads tags; elsewhere a direction is dropped with a
warning and the emotion stays a face-only cue, as before.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

#### synthesize(text, voice_id=None, , model_id=None, voice_settings=None, seed=None, audio_tags=None, \*\*kw)

Speak `text`. The clip’s `transcript` is `text` WITHOUT the tags,
so alignment and captions never read a cue.

* **Return type:**
  [`AudioClip`](_autosummary/an.audio.tts.html.md#an.audio.tts.AudioClip)

#### take_options(options, take)

The request for candidate `take` of a best-of-N line ([`an.audio.takes`](_autosummary/an.audio.takes.html.md#module-an.audio.takes)).

A declared `seed` is offset by the take, so a model that does honour
it (every model but `eleven_v3`, measured) still returns different
takes; without a seed the request is unchanged and the model’s own
sampling varies the take.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> ElevenLabsTTS(api_key="unused").take_options({"seed": 11}, 2)
{'seed': 13}
```

### *exception* an.audio.elevenlabs_tts.ElevenLabsUnavailableError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A request cannot be sent: no API key is configured.

### *exception* an.audio.elevenlabs_tts.ElevenLabsVoiceError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A voice document declares ElevenLabs settings that are malformed.

### an.audio.elevenlabs_tts.VOICE_SETTINGS_RANGES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)]* *= {'similarity_boost': (0.0, 1.0), 'speed': (0.7, 1.2), 'stability': (0.0, 1.0), 'style': (0.0, 1.0), 'use_speaker_boost': None}*

The `voice_settings` keys the API takes, with the range each accepts
(`None` = a boolean). `speed` is the API’s documented 0.7–1.2.

### an.audio.elevenlabs_tts.normalize_voice_settings(raw)

The canonical `voice_settings` dict — only what was declared, key-sorted.

Omit-when-unset: `None` and `{}` give `{}`. Unknown keys and values out
of range raise, so a typo cannot silently fall back to the account default.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> normalize_voice_settings({"style": 1, "stability": 0.25})
{'stability': 0.25, 'style': 1.0}
>>> normalize_voice_settings({"stabilty": 0.5})
Traceback (most recent call last):
    ...
an.audio.elevenlabs_tts.ElevenLabsVoiceError: unknown voice_settings key(s) ['stabilty']; known: ['similarity_boost', 'speed', 'stability', 'style', 'use_speaker_boost']
```

### an.audio.elevenlabs_tts.tagged_text(text, tags)

`text` with each tag prefixed as `[tag]` — what an audio-tag model reads.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.audio.elevenlabs_tts.takes_audio_tags(model_id, , prefixes=('eleven_v3', 'eleven_v4'))

Whether `model_id` reads inline audio tags.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> takes_audio_tags("eleven_v3"), takes_audio_tags("eleven_turbo_v2_5")
(True, False)
```


# _autosummary/an.audio.html.md

# an.audio

Audio pipeline — TTS and lip-sync providers + orchestration.

Phase 3 ships:

- Protocols: `TTSProvider`, `LipSyncProvider` (Phase 1).
- Default offline provider: `OfflineTTS` (silent WAV). The lip-sync providers
  (`OfflineLipSync` deterministic char-to-viseme, `RhubarbLipSync`,
  `WhisperLipSync`) moved to `cutan` with the cut-out genre (an#225); the old
  names still resolve from here, with a warning.
- Real providers: `ElevenLabsTTS` (needs `ELEVEN_API_KEY`).
- Orchestration: `produce_audio_for_dialogue` /
  `produce_audio_for_scene` walk a SceneIR, synthesize, persist to mall,
  stamp viseme tracks back onto the IR.

The defaults are intentionally offline so the entire `an` pipeline works
without external services.

### Functions

| [`word_timings_to_visemes`](_autosummary/an.audio.html.md#an.audio.word_timings_to_visemes)(words, \*, ...[, ...])    | Distribute viseme keyframes across word boundaries.                                                                                                                                                                     |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`default_tts`](_autosummary/an.audio.html.md#an.audio.default_tts)()                                     | The default TTS provider: `OfflineTTS`.                                                                                                                                                                                 |
| [`default_lipsync`](_autosummary/an.audio.html.md#an.audio.default_lipsync)()                                 | The default lip-sync provider: the loaded genre's `offline` one (the deterministic char-to-viseme provider of `cutan`), else [`NullLipSync`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.NullLipSync). |
| [`produce_audio_for_dialogue`](_autosummary/an.audio.html.md#an.audio.produce_audio_for_dialogue)(dialogue[, mall, ...]) | Synthesize audio + visemes for one dialogue line.                                                                                                                                                                       |
| [`produce_audio_for_scene`](_autosummary/an.audio.html.md#an.audio.produce_audio_for_scene)(scene[, mall, tts, ...])  | Walk every dialogue line, synthesize, and stamp viseme tracks back.                                                                                                                                                     |
| [`make_tts`](_autosummary/an.audio.html.md#an.audio.make_tts)(name)                                    | Instantiate a TTS provider by name.                                                                                                                                                                                     |
| [`make_lipsync`](_autosummary/an.audio.html.md#an.audio.make_lipsync)(name, \*[, language])                | Instantiate a LipSync provider by name.                                                                                                                                                                                 |
| [`known_tts_names`](_autosummary/an.audio.html.md#an.audio.known_tts_names)()                                 | Return the registered TTS provider names.                                                                                                                                                                               |
| [`known_lipsync_names`](_autosummary/an.audio.html.md#an.audio.known_lipsync_names)()                             | Return the registered LipSync provider names.                                                                                                                                                                           |

### Classes

| [`TTSProvider`](_autosummary/an.audio.html.md#an.audio.TTSProvider)(\*args, \*\*kwargs)                   | Text-to-speech provider.                                                 |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`AudioClip`](_autosummary/an.audio.html.md#an.audio.AudioClip)([path, bytes_, duration, ...])          | A rendered audio clip, on disk or in memory.                             |
| [`VoiceMeta`](_autosummary/an.audio.html.md#an.audio.VoiceMeta)(voice_id, name, provider[, ...])        | Metadata for a TTS voice as exposed by a provider.                       |
| [`LipSyncProvider`](_autosummary/an.audio.html.md#an.audio.LipSyncProvider)(\*args, \*\*kwargs)               | Audio + transcript → aligned viseme track.                               |
| [`Viseme`](_autosummary/an.audio.html.md#an.audio.Viseme)(time, code[, intensity])                   | A single mouth-shape keyframe.                                           |
| [`VisemeTrack`](_autosummary/an.audio.html.md#an.audio.VisemeTrack)([visemes, convention, duration, ...]) | Aligned viseme sequence produced by a LipSyncProvider.                   |
| [`WordTimingProvider`](_autosummary/an.audio.html.md#an.audio.WordTimingProvider)(\*args, \*\*kwargs)            | Audio → `[(word, start_s, end_s), ...]`.                                 |
| [`NullLipSync`](_autosummary/an.audio.html.md#an.audio.NullLipSync)()                                     | The lip-sync provider of a core without a genre: no mouth shapes at all. |
| [`OfflineTTS`](_autosummary/an.audio.html.md#an.audio.OfflineTTS)(\*[, sample_rate, channels, ...])      | Default TTS provider: silent WAV of length proportional to text.         |
| [`ElevenLabsTTS`](_autosummary/an.audio.html.md#an.audio.ElevenLabsTTS)(\*[, api_key, model_id, ...])       | ElevenLabs-backed TTSProvider.                                           |
| [`MacSayTTS`](_autosummary/an.audio.html.md#an.audio.MacSayTTS)(\*[, default_voice_id, ...])            | macOS `say`-backed TTSProvider.                                          |

### *class* an.audio.AudioClip(path=None, bytes_=None, duration=0.0, sample_rate=44100, channels=1, voice_id=None, transcript=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A rendered audio clip, on disk or in memory.

### *class* an.audio.ElevenLabsTTS(, api_key=None, model_id='eleven_turbo_v2_5', output_format='mp3_44100_128', audio_tag_model_prefixes=('eleven_v3', 'eleven_v4'), client_factory=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

ElevenLabs-backed TTSProvider. Constructor takes an optional api_key
(falls back to `ELEVEN_API_KEY` / `ELEVENLABS_API_KEY`).

Implements the `TTSProvider` protocol, plus the optional
`synthesis_options` hook the audio pipeline reads (an#209).

#### billed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

what may never run in memory).

* **Type:**
  Every request is charged (an#311

#### billed_characters(text, , audio_tags=None, \*\*\_options)

Characters one request bills: the text as sent, audio tags included.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> ElevenLabsTTS(api_key="unused").billed_characters("Hi!", audio_tags=["excited"])
13
```

#### check_available()

Raise `ElevenLabsUnavailableError` when a request could not be
sent (no API key) — what the audio pipeline asks before the first
request of a render, so nothing is half-synthesized (an#305).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### client_factory

`api_key -> client`; tests inject a fake so nothing reaches the API.

#### list_voices(, search=None)

The account’s voices (its own plus the ones it saved), newest API.

`search` filters by name, description and labels on the server. An
absent key or SDK yields `[]`; a key that is present and a call that
fails RAISES — an empty listing must mean “no voices”, not “it broke”.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`VoiceMeta`](_autosummary/an.audio.tts.html.md#an.audio.tts.VoiceMeta)]

#### synthesis_options(voice, , emotion=None, direction=None)

The `synthesize` keyword arguments a voice document and a line imply.

Only what is declared appears (`{}` for a plain voice), so the audio
cache key — which includes this dict when it is non-empty — is unchanged
for every voice that declares nothing new. `audio_tags` are the line’s
`[emotion]` (unless `neutral`) then its `direction` cues, and only
on a model that reads tags; elsewhere a direction is dropped with a
warning and the emotion stays a face-only cue, as before.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

#### synthesize(text, voice_id=None, , model_id=None, voice_settings=None, seed=None, audio_tags=None, \*\*kw)

Speak `text`. The clip’s `transcript` is `text` WITHOUT the tags,
so alignment and captions never read a cue.

* **Return type:**
  [`AudioClip`](_autosummary/an.audio.tts.html.md#an.audio.tts.AudioClip)

#### take_options(options, take)

The request for candidate `take` of a best-of-N line ([`an.audio.takes`](_autosummary/an.audio.takes.html.md#module-an.audio.takes)).

A declared `seed` is offset by the take, so a model that does honour
it (every model but `eleven_v3`, measured) still returns different
takes; without a seed the request is unchanged and the model’s own
sampling varies the take.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> ElevenLabsTTS(api_key="unused").take_options({"seed": 11}, 2)
{'seed': 13}
```

### *class* an.audio.LipSyncProvider(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Audio + transcript → aligned viseme track.

A provider that fills `VisemeTrack.words` also declares
`emits_word_timings = True` (a plain class attribute, absent means
False). The audio pipeline reads it to decide whether a line whose
`word_timings` are missing needs re-alignment — without the flag, a
provider that cannot supply words would re-align every line forever.

#### align(audio, transcript)

Produce a viseme track for `audio` given its `transcript`.

* **Return type:**
  [`VisemeTrack`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.VisemeTrack)

### *class* an.audio.MacSayTTS(, default_voice_id='Samantha', sample_rate=22050, rate_wpm=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

macOS `say`-backed TTSProvider.

Implements the `TTSProvider` protocol. Audible, deterministic, and
fully offline — uses Apple’s voice synthesis bundled with the OS.

#### billed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= False*

an#311).

* **Type:**
  Costs nothing per request (stated, never inferred

#### repeatable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

The same request gives the same audio, so best-of-N takes never apply
([`an.audio.takes.voice_takes()`](_autosummary/an.audio.takes.html.md#an.audio.takes.voice_takes)) and nothing is billed.

### *class* an.audio.NullLipSync

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The lip-sync provider of a core without a genre: no mouth shapes at all.

A genre that animates mouths (`cutan`) registers real providers
(`offline`, `rhubarb`, `whisper`); without one, a line’s audio is still
synthesized and the track it gets is empty – nothing draws a mouth anyway.

```pycon
>>> NullLipSync().align(AudioClip(bytes_=b"", duration=0.5), "hi").duration
0.5
```

#### repeatable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

a reader
of the cache keys may run it in memory (an#311).

* **Type:**
  The same request gives the same track, and nothing is billed

### *class* an.audio.OfflineTTS(, sample_rate=22050, channels=1, seconds_per_char=0.06)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Default TTS provider: silent WAV of length proportional to text.

Implements the `TTSProvider` protocol.

#### billed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= False*

an#311).

* **Type:**
  Costs nothing per request (stated, never inferred

#### repeatable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

The same request gives the same audio, so best-of-N takes never apply
([`an.audio.takes.voice_takes()`](_autosummary/an.audio.takes.html.md#an.audio.takes.voice_takes)) and nothing is billed.

#### silent *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

a line it speaks for a voice written for a real
provider is a stand-in the render says so loudly (an#305).

* **Type:**
  Its audio is silence

### *class* an.audio.TTSProvider(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Text-to-speech provider.

#### list_voices()

Return all voices the provider exposes.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`VoiceMeta`](_autosummary/an.audio.tts.html.md#an.audio.tts.VoiceMeta)]

#### synthesize(text, voice_id, \*\*kw)

Render `text` in `voice_id`’s voice. Returns an AudioClip.

* **Return type:**
  [`AudioClip`](_autosummary/an.audio.tts.html.md#an.audio.tts.AudioClip)

### *class* an.audio.Viseme(time, code, intensity=1.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single mouth-shape keyframe.

### *class* an.audio.VisemeTrack(visemes=<factory>, convention='rhubarb', duration=0.0, words=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Aligned viseme sequence produced by a LipSyncProvider.

`words` carries the word timings the provider aligned from, when it had
any (whisper, `WordTimingsLipSync`);
`None` for providers that never see words (offline, Rhubarb — whose JSON
is mouth cues only). Retained since an#96 rather than discarded after the
viseme conversion: captions (Wave 8) and any consumer that wants to know
*which word* a mouth shape belongs to read them from the IR.

### *class* an.audio.VoiceMeta(voice_id, name, provider, language='en', gender=None, extra=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Metadata for a TTS voice as exposed by a provider.

### *class* an.audio.WordTimingProvider(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Audio → `[(word, start_s, end_s), ...]`.

Implementations may run a transcriber (Whisper / Scribe) or look up
pre-computed timings (`muvid`’s lacing store). Returned tuples are
expected to be in ascending start-time order; gaps between words are
fine and represent silence the lipsync provider should rest through.

#### words_for(audio, , transcript='')

Return the word timings for `audio`.

* **Return type:**
  [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### an.audio.default_lipsync()

The default lip-sync provider: the loaded genre’s `offline` one (the
deterministic char-to-viseme provider of `cutan`), else
[`NullLipSync`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.NullLipSync).

* **Return type:**
  [`LipSyncProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.LipSyncProvider)

```pycon
>>> default_lipsync().name in {"offline", "none"}
True
```

### an.audio.default_tts()

The default TTS provider: `OfflineTTS`.

* **Return type:**
  [`TTSProvider`](_autosummary/an.audio.tts.html.md#an.audio.tts.TTSProvider)

### an.audio.known_lipsync_names()

Return the registered LipSync provider names.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.audio.known_tts_names()

Return the registered TTS provider names.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.audio.make_lipsync(name, , language='en')

Instantiate a LipSync provider by name.

`language` (BCP-47) reaches providers that select behaviour by language —
Rhubarb’s recognizer today; a future aligner’s weight allowlist.

The default name (`"offline"`) resolves to [`NullLipSync`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.NullLipSync)
when no loaded genre provides it, so voicing a line needs no genre; any other
name that nothing provides is an error that says how to get it.

* **Return type:**
  [`LipSyncProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.LipSyncProvider)

```pycon
>>> make_lipsync("offline").name in {"offline", "none"}
True
```

### an.audio.make_tts(name)

Instantiate a TTS provider by name.

Raises `ValueError` for unknown names with a list of known options.

* **Return type:**
  [`TTSProvider`](_autosummary/an.audio.tts.html.md#an.audio.tts.TTSProvider)

### an.audio.produce_audio_for_dialogue(dialogue, mall=None, \*, tts=None, lipsync=None, effects=None, voice_id=None, takes=<object object>, take_scorer=<function make_take_scorer>)

Synthesize audio + visemes for one dialogue line.

Side effects: when `mall` is provided, persists the WAV to
`mall["audio"]` keyed by the content-hash of the dialogue, and persists
the viseme JSON to `mall["visemes"]` similarly. Cache-friendly: a
second call with identical inputs returns the cached versions.

`effects` (default: what the line’s voice declares in `mall["voices"]`)
is applied to the synthesized audio BEFORE alignment, so the visemes are
computed on the audio the viewer hears. The raw synthesis stays cached under
its own key, so changing an effect never re-pays the TTS.

`tts` (default: the provider the voice document declares, else offline —
`tts_chooser()`) is an override when given.

`voice_id` (default: the line’s `voice_ref`, else `"default"`) is the
`voices`-store key; [`produce_audio_for_scene()`](_autosummary/an.audio.html.md#an.audio.produce_audio_for_scene) passes the one
[`an.audio.voices.line_voice_id()`](_autosummary/an.audio.voices.html.md#an.audio.voices.line_voice_id) resolves, so a character’s bound
voice reaches here (an#194). The provider is handed the voice document’s
own voice id when it names one.

What the provider’s optional `synthesis_options` hook derives from the
voice document and the line (ElevenLabs: `model_id`, `voice_settings`,
`seed`, and the `[emotion]`/`direction` audio tags — an#209) is passed
to `synthesize` and keyed; the text handed to alignment is always the
bare `dialogue.text`, never the tagged one.

`takes` (default: what the voice declares for this line — see
[`an.audio.takes`](_autosummary/an.audio.takes.html.md#module-an.audio.takes); `None` forces one take) synthesizes several takes,
scores each with `take_scorer(spec)` on the audio the viewer hears, keeps
the best and records the choice in `mall["takes"]`. A recorded choice is
restored from the record and never re-rolled; a recorded take whose audio is
gone raises [`TakeLostError`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeLostError) before any request.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`AudioClip`](_autosummary/an.audio.tts.html.md#an.audio.tts.AudioClip), [`VisemeTrack`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.VisemeTrack)]

### an.audio.produce_audio_for_scene(scene, mall=None, \*, tts=None, lipsync=None, take_scorer=<function make_take_scorer>, announce=<function \_announce_to_stderr>, overruns=True, strict=False, tts_factory=None)

Walk every dialogue line, synthesize, and stamp viseme tracks back.

Mutates the `scene` in place AND returns it (for chaining).
Stamps `Dialogue.duration`, `Dialogue.viseme_track`, and
`Dialogue.audio_ref` (mall[“audio”] key) so the renderer can find the
audio later. Lines with an existing viseme_track AND audio_ref are not
re-synthesized (idempotent).

`Dialogue.start` is DERIVED on every pass, synthesized or not, by
`Dialogue.planned_start()`: the line’s `at` if set, else the previous
line’s end plus its `pause` (an#187). So editing a pause re-times the
shot without touching the audio, and every consumer of `start` — the
mux, the visemes, captions, ducking — follows. A `start` on a line that
was never synthesized is an authored start from before `at` existed,
and is kept as the line’s `at`.

Every line’s request is resolved BEFORE anything is synthesized, so a
malformed voice (an effect, a `takes`), a corrupt takes record or a
recorded take whose audio is gone fails before a credit is spent; and
`announce` (default: a line on stderr; `None` for silence) is told, before the first request, what best-of-N takes will bill (requests
and the provider’s characters) and which recorded takes were chosen by an
older scorer version than the current one (they are kept). After synthesis,
a line that ends past its shot’s end (`dialogue_overruns()`) is
announced too — or, with `announce=None`, a `DialogueOverrunWarning` —
unless `overruns=False`: `an render` passes that, because it reports
every post-synthesis finding together in its summary (an#254).

**Who speaks each line** (an#305, `tts_chooser()`): with `tts` not
given (`None`, `""` or `VOICE_TTS`), the line’s voice document’s
`provider`, built by `tts_factory` (default
[`an.audio.providers.make_tts()`](_autosummary/an.audio.providers.html.md#an.audio.providers.make_tts)), else offline; a `tts` given speaks
every line. A line spoken by another provider than its voice declares is a
`VoiceStandInWarning` — the silent offline voice says so — and,
with `strict`, a `VoiceStandInError` before any request. A provider
with requests to send is asked `check_available()` first (ElevenLabs: is
there a key?), and what a provider that bills per character will bill is
announced before the first request — whatever chose the provider; a cached
line is never billed and never counted.

* **Return type:**
  [`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR)

### an.audio.word_timings_to_visemes(words, , total_duration, char_to_viseme, rest_viseme='X', min_gap_for_rest=0.2)

Distribute viseme keyframes across word boundaries.

Used by `WhisperLipSync` and `WordTimingsLipSync`. The
algorithm: per word, walk its characters in order, look up each
character’s viseme code, dedupe consecutive identical codes, and
space the resulting keyframes evenly across the word’s
`[start, end]` interval. In gaps wider than `min_gap_for_rest` —
between two words, or after the last one before `total_duration` —
insert a single rest keyframe just after the previous word ended.

Times are clamped to `[0, total_duration]` since some
transcribers occasionally round the last word’s end past the
audio’s actual length.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Viseme`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.Viseme)]

### Modules

| [`cli`](_autosummary/an.audio.cli.html.md#module-an.audio.cli)                       | `an voices ...` — browse a TTS provider's voices from the shell (an#209).            |
|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`effects`](_autosummary/an.audio.effects.html.md#module-an.audio.effects)               | Voice effects: a deterministic transform applied to a synthesized line (an#163).     |
| [`elevenlabs_tts`](_autosummary/an.audio.elevenlabs_tts.html.md#module-an.audio.elevenlabs_tts) | ElevenLabsTTS — real speech via the ElevenLabs API.                                  |
| [`lipsync`](_autosummary/an.audio.lipsync.html.md#module-an.audio.lipsync)               | Lip-sync provider protocol + viseme dataclasses.                                     |
| [`loudness`](_autosummary/an.audio.loudness.html.md#module-an.audio.loudness)             | Voice loudness: one integrated loudness for every voice of a film (an#315).          |
| [`mac_say_tts`](_autosummary/an.audio.mac_say_tts.html.md#module-an.audio.mac_say_tts)       | MacSayTTS — audible offline speech via macOS's built-in `say` command.               |
| [`offline_tts`](_autosummary/an.audio.offline_tts.html.md#module-an.audio.offline_tts)       | OfflineTTS — produces silent audio of plausible duration.                            |
| [`pipeline`](_autosummary/an.audio.pipeline.html.md#module-an.audio.pipeline)             | Audio pipeline orchestration: dialogue → audio → visemes → IR mutation.              |
| [`providers`](_autosummary/an.audio.providers.html.md#module-an.audio.providers)           | Provider factory: name → concrete TTS/LipSync provider instance.                     |
| [`takes`](_autosummary/an.audio.takes.html.md#module-an.audio.takes)                   | Best-of-N takes: re-roll a line, score every take, keep the best, record the choice. |
| [`tts`](_autosummary/an.audio.tts.html.md#module-an.audio.tts)                       | TTS provider protocol + supporting dataclasses.                                      |
| [`voices`](_autosummary/an.audio.voices.html.md#module-an.audio.voices)                 | Which voice speaks a dialogue line: the character → voice binding (an#194).          |


# _autosummary/an.audio.lipsync.html.md

# an.audio.lipsync

Lip-sync provider protocol + viseme dataclasses.

Viseme conventions are renderer-agnostic in the IR; concrete providers in
later phases (Rhubarb, Azure, MFA) emit their own letter/number encodings,
which the cutout adapter then maps to mouth-slot attachments.

Two protocols live here:

- [`LipSyncProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.LipSyncProvider) — the head-to-toe pipeline (“audio + transcript
  → visemes”). Owns its own transcription if it needs one. Most cutout
  callers go through this.
- [`WordTimingProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.WordTimingProvider) — the narrower “audio → word timings”
  contract. Lets external callers (e.g. `muvid`, with its own lyric
  alignment store) skip a redundant transcription pass and feed timings
  directly into `WordTimingsLipSync`.

### Module Attributes

| [`WordTiming`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.WordTiming)   | One word's slice in time.   |
|---------------------------------------------------------------|-----------------------------|

### Functions

| [`word_timings_to_visemes`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.word_timings_to_visemes)(words, \*, ...[, ...])   | Distribute viseme keyframes across word boundaries.   |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------|

### Classes

| [`LipSyncProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.LipSyncProvider)(\*args, \*\*kwargs)               | Audio + transcript → aligned viseme track.                               |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`NullLipSync`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.NullLipSync)()                                     | The lip-sync provider of a core without a genre: no mouth shapes at all. |
| [`Viseme`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.Viseme)(time, code[, intensity])                   | A single mouth-shape keyframe.                                           |
| [`VisemeTrack`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.VisemeTrack)([visemes, convention, duration, ...]) | Aligned viseme sequence produced by a LipSyncProvider.                   |
| [`WordTimingProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.WordTimingProvider)(\*args, \*\*kwargs)            | Audio → `[(word, start_s, end_s), ...]`.                                 |

### *class* an.audio.lipsync.LipSyncProvider(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Audio + transcript → aligned viseme track.

A provider that fills `VisemeTrack.words` also declares
`emits_word_timings = True` (a plain class attribute, absent means
False). The audio pipeline reads it to decide whether a line whose
`word_timings` are missing needs re-alignment — without the flag, a
provider that cannot supply words would re-align every line forever.

#### align(audio, transcript)

Produce a viseme track for `audio` given its `transcript`.

* **Return type:**
  [`VisemeTrack`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.VisemeTrack)

### *class* an.audio.lipsync.NullLipSync

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The lip-sync provider of a core without a genre: no mouth shapes at all.

A genre that animates mouths (`cutan`) registers real providers
(`offline`, `rhubarb`, `whisper`); without one, a line’s audio is still
synthesized and the track it gets is empty – nothing draws a mouth anyway.

```pycon
>>> NullLipSync().align(AudioClip(bytes_=b"", duration=0.5), "hi").duration
0.5
```

#### repeatable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

a reader
of the cache keys may run it in memory (an#311).

* **Type:**
  The same request gives the same track, and nothing is billed

### *class* an.audio.lipsync.Viseme(time, code, intensity=1.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single mouth-shape keyframe.

### *class* an.audio.lipsync.VisemeTrack(visemes=<factory>, convention='rhubarb', duration=0.0, words=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Aligned viseme sequence produced by a LipSyncProvider.

`words` carries the word timings the provider aligned from, when it had
any (whisper, `WordTimingsLipSync`);
`None` for providers that never see words (offline, Rhubarb — whose JSON
is mouth cues only). Retained since an#96 rather than discarded after the
viseme conversion: captions (Wave 8) and any consumer that wants to know
*which word* a mouth shape belongs to read them from the IR.

### an.audio.lipsync.WordTiming

One word’s slice in time. Tuple form keeps providers cheap.

alias of [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### *class* an.audio.lipsync.WordTimingProvider(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Audio → `[(word, start_s, end_s), ...]`.

Implementations may run a transcriber (Whisper / Scribe) or look up
pre-computed timings (`muvid`’s lacing store). Returned tuples are
expected to be in ascending start-time order; gaps between words are
fine and represent silence the lipsync provider should rest through.

#### words_for(audio, , transcript='')

Return the word timings for `audio`.

* **Return type:**
  [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### an.audio.lipsync.word_timings_to_visemes(words, , total_duration, char_to_viseme, rest_viseme='X', min_gap_for_rest=0.2)

Distribute viseme keyframes across word boundaries.

Used by `WhisperLipSync` and `WordTimingsLipSync`. The
algorithm: per word, walk its characters in order, look up each
character’s viseme code, dedupe consecutive identical codes, and
space the resulting keyframes evenly across the word’s
`[start, end]` interval. In gaps wider than `min_gap_for_rest` —
between two words, or after the last one before `total_duration` —
insert a single rest keyframe just after the previous word ended.

Times are clamped to `[0, total_duration]` since some
transcribers occasionally round the last word’s end past the
audio’s actual length.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Viseme`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.Viseme)]


# _autosummary/an.audio.loudness.html.md

# an.audio.loudness

Voice loudness: one integrated loudness for every voice of a film (an#315).

TTS providers deliver voices at very different levels — measured on a four-voice
ElevenLabs episode: -16.4, -35.5, -28.4 and -23.4 LUFS, a 19 dB spread that left
one character barely audible. `meta.voice_loudness` ([`VoiceLoudness`](_autosummary/an.ir.schema.html.md#an.ir.schema.VoiceLoudness))
asks for one level for all of them:

- each voice’s integrated loudness (EBU R128 / ITU-R BS.1770, measured by
  ffmpeg’s `ebur128` over ALL of that voice’s lines in the film, back to
  back: a 0.6 s line has a single 400 ms gating block, and a voice’s own
  dynamics between lines — a whisper, a shout — are kept);
- ONE gain per voice, `target - measured` plus the voice document’s
  `loudness_offset_db`, rounded to [`GAIN_STEP_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.GAIN_STEP_DB), then a lookahead
  peak limiter at `peak_db` ([`limit_peaks()`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.limit_peaks): a sample never passes the
  ceiling, and the gain eases into and out of a peak rather than clipping);
- the result is DERIVED audio, content-keyed in the `audio` store by
  (the line’s audio, the gain, the ceiling, [`LEVEL_VERSION`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.LEVEL_VERSION)) — never a
  re-synthesis, never billed — and the line is stamped with it, so the
  per-shot mux, the film mix and every cache key follow it. The gain changes
  no timing: lip-sync, word timings and durations are the source line’s.

A voice whose lines are silence (the offline provider) is left as it is. A
voice whose lines are all short is measured looped ([`MIN_MEASURE_S`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.MIN_MEASURE_S)).
The gain is capped at [`MAX_GAIN_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.MAX_GAIN_DB), and a voice that ends more than
[`MISS_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.MISS_WARNING_DB) from its target (capped, or held down by its peaks) is
warned about. `peak_db` is a SAMPLE-peak ceiling: an AAC encode can still
overshoot it between samples by a fraction of a dB, so keep a margin.

The gain and the source are stamped on each line (`Dialogue.leveled`): a
render whose voice is unchanged reuses them without decoding or measuring
anything, and a re-measure that moves the gain by less than a step keeps the
previous gain (hysteresis), so one new line rarely re-renders a voice’s other
shots.
Without a target, [`loudness_report()`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.loudness_report) measures the voices and
[`voice_loudness_spread()`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.voice_loudness_spread) says when they differ by more than
[`SPREAD_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.SPREAD_WARNING_DB) — the render warns, naming each voice’s level.

```pycon
>>> float(limit_peaks(__import__("numpy").array([[0.0], [2.0], [0.0]]), ceiling=1.0).max())
1.0
```

### Module Attributes

| [`LEVEL_VERSION`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.LEVEL_VERSION)     | Bumped whenever a leveled line's bytes would change for the same inputs (the limiter, its window, the encoding); part of every leveled line's key.                                                                                  |
|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`GAIN_STEP_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.GAIN_STEP_DB)      | A voice's gain is rounded to this, so a new line that moves its voice's loudness by less than half of it re-renders nothing else (half a dB is below what a listener hears between two lines; the measurement itself is to 0.1 LU). |
| [`SILENCE_LUFS`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.SILENCE_LUFS)      | never leveled.                                                                                                                                                                                                                      |
| [`SPREAD_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.SPREAD_WARNING_DB) | Voices further apart than this, with no target set, are warned about.                                                                                                                                                               |
| [`LIMITER_WINDOW_S`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.LIMITER_WINDOW_S)  | the gain eases into a peak and back out over this much on each side, so nothing below ~20 Hz is modulated within a cycle (a 5 ms release distorted anything under 100 Hz, an#315 review).                                           |
| [`MAX_GAIN_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.MAX_GAIN_DB)       | The largest gain leveling gives a voice; beyond it the voice is mostly noise (room tone measured at -62 LUFS asked for +46 dB).                                                                                                     |
| [`OFFSET_LIMITS_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.OFFSET_LIMITS_DB)  | `loudness_offset_db` bounds on a voice document.                                                                                                                                                                                    |
| [`MIN_MEASURE_S`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.MIN_MEASURE_S)     | EBU R128 gates in 400 ms blocks, and a voice of only short lines would read as silence.                                                                                                                                             |
| [`MISS_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.MISS_WARNING_DB)   | A leveled voice that ends further than this from its target (the limiter held its peaks, or the gain was capped) is warned about.                                                                                                   |

### Functions

| [`integrated_loudness`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.integrated_loudness)(pcm)                    | EBU R128 integrated loudness (LUFS) of `pcm`, by ffmpeg's `ebur128` (`-inf` for silence).                                                                                                                                                                                                                                   |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`level_voices`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.level_voices)(scene, mall, spec)             | Level every voice of `scene` to `spec` ([`VoiceLoudness`](_autosummary/an.ir.schema.html.md#an.ir.schema.VoiceLoudness)): each line is stamped with its leveled audio (made once per source bytes, gain and ceiling, in the `audio` store) and `Dialogue.leveled`.                                                |
| [`leveled_audio`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.leveled_audio)(audio, \*, gain_db, peak_db)  | `audio` at `gain_db`, its peaks held at `peak_db` dBFS, as a 16-bit PCM WAV of exactly the length of the decoded source (an MP3 source is stored decoded, as the mux would decode it).                                                                                                                                      |
| [`leveled_key`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.leveled_key)(source, \*, gain_db, peak_db)   | The content key of a leveled line: the sha256 of its source audio's BYTES (not the request that made them: a take re-synthesised under the same request is other audio, an#315 review), the gain, the ceiling, the rate a non-WAV source is decoded at, and [`LEVEL_VERSION`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.LEVEL_VERSION). |
| [`limit_peaks`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.limit_peaks)(samples, \*, ceiling[, window]) | `samples` with no sample above `ceiling` (absolute, full scale 1.0).                                                                                                                                                                                                                                                        |
| [`loudness_report`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.loudness_report)(scene, mall)                | Every voice's integrated loudness over its lines, as heard now (a leveled line's leveled audio).                                                                                                                                                                                                                            |
| [`voice_loudness_spread`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.voice_loudness_spread)(levels)               | What to say when the audible voices differ by more than [`SPREAD_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.SPREAD_WARNING_DB); `None` otherwise.                                                                                                                                                                               |
| [`voiced_lines`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.voiced_lines)(scene, mall)                   | `{voice id: [line, ...]}` for every line with audio in the store.                                                                                                                                                                                                                                                           |
| [`voiced_sources`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.voiced_sources)(scene, mall)                 | [`voiced_lines()`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.voiced_lines), by each line's SOURCE audio (a leveled line's own).                                                                                                                                                                                                        |

### Classes

| [`VoiceLevel`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.VoiceLevel)(voice, lufs[, gain_db, lines])   | One voice's measured loudness, and the gain leveling gives it.   |
|----------------------------------------------------------------------------------------------|------------------------------------------------------------------|

### Exceptions

| [`VoiceLoudnessError`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.VoiceLoudnessError)   | The voices cannot be measured or leveled (no ffmpeg, an unreadable line).                                             |
|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| [`VoiceLoudnessWarning`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.VoiceLoudnessWarning) | The film's voices differ in loudness by more than [`SPREAD_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.SPREAD_WARNING_DB). |

### an.audio.loudness.GAIN_STEP_DB *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.5*

A voice’s gain is rounded to this, so a new line that moves its voice’s
loudness by less than half of it re-renders nothing else (half a dB is
below what a listener hears between two lines; the measurement itself is
to 0.1 LU).

### an.audio.loudness.LEVEL_VERSION *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 1*

Bumped whenever a leveled line’s bytes would change for the same inputs
(the limiter, its window, the encoding); part of every leveled line’s key.
A test pins the leveled output to it.

### an.audio.loudness.LIMITER_WINDOW_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.05*

the gain eases into a peak
and back out over this much on each side, so nothing below ~20 Hz is
modulated within a cycle (a 5 ms release distorted anything under 100 Hz,
an#315 review).

* **Type:**
  The limiter’s lookahead and release, seconds

### an.audio.loudness.MAX_GAIN_DB *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 24.0*

The largest gain leveling gives a voice; beyond it the voice is mostly
noise (room tone measured at -62 LUFS asked for +46 dB). Capped, and warned.

### an.audio.loudness.MIN_MEASURE_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 1.0*

EBU R128 gates
in 400 ms blocks, and a voice of only short lines would read as silence.

* **Type:**
  A voice shorter than this is measured looped to this length

### an.audio.loudness.MISS_WARNING_DB *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 1.0*

A leveled voice that ends further than this from its target (the limiter
held its peaks, or the gain was capped) is warned about.

### an.audio.loudness.OFFSET_LIMITS_DB *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= (-12.0, 12.0)*

`loudness_offset_db` bounds on a voice document.

### an.audio.loudness.SILENCE_LUFS *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= -70.0*

never leveled.

* **Type:**
  Below this a voice is silence (BS.1770’s absolute gate)

### an.audio.loudness.SPREAD_WARNING_DB *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 6.0*

Voices further apart than this, with no target set, are warned about.

### *class* an.audio.loudness.VoiceLevel(voice, lufs, gain_db=0.0, lines=0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One voice’s measured loudness, and the gain leveling gives it.

### *exception* an.audio.loudness.VoiceLoudnessError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The voices cannot be measured or leveled (no ffmpeg, an unreadable line).

### *exception* an.audio.loudness.VoiceLoudnessWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

The film’s voices differ in loudness by more than [`SPREAD_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.SPREAD_WARNING_DB).

### an.audio.loudness.integrated_loudness(pcm)

EBU R128 integrated loudness (LUFS) of `pcm`, by ffmpeg’s `ebur128`
(`-inf` for silence).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### an.audio.loudness.level_voices(scene, mall, spec)

Level every voice of `scene` to `spec` ([`VoiceLoudness`](_autosummary/an.ir.schema.html.md#an.ir.schema.VoiceLoudness)):
each line is stamped with its leveled audio (made once per source bytes,
gain and ceiling, in the `audio` store) and `Dialogue.leveled`.
Returns each voice’s level; warns ([`VoiceLoudnessWarning`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.VoiceLoudnessWarning)) about a
voice it had to cap or could not bring within [`MISS_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.MISS_WARNING_DB).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`VoiceLevel`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.VoiceLevel)]

### an.audio.loudness.leveled_audio(audio, , gain_db, peak_db)

`audio` at `gain_db`, its peaks held at `peak_db` dBFS, as a 16-bit
PCM WAV of exactly the length of the decoded source (an MP3 source is
stored decoded, as the mux would decode it).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### an.audio.loudness.leveled_key(source, , gain_db, peak_db)

The content key of a leveled line: the sha256 of its source audio’s
BYTES (not the request that made them: a take re-synthesised under the
same request is other audio, an#315 review), the gain, the ceiling, the
rate a non-WAV source is decoded at, and [`LEVEL_VERSION`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.LEVEL_VERSION).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> leveled_key(b"a", gain_db=1.0, peak_db=-1.5) != leveled_key(b"a", gain_db=1.1, peak_db=-1.5)
True
```

### an.audio.loudness.limit_peaks(samples, , ceiling, window=1)

`samples` with no sample above `ceiling` (absolute, full scale 1.0).

The gain each sample needs (`ceiling / |x|`, at most 1) is spread over
`window` samples on both sides — a sliding minimum over `2 * window + 1`,
then a moving average over `window` — so it eases into and out of a peak.
Every sample’s gain is at most what that sample needs, so the ceiling holds
exactly; nothing moves in time.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.audio.loudness.loudness_report(scene, mall)

Every voice’s integrated loudness over its lines, as heard now (a
leveled line’s leveled audio).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`VoiceLevel`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.VoiceLevel)]

### an.audio.loudness.voice_loudness_spread(levels)

What to say when the audible voices differ by more than
[`SPREAD_WARNING_DB`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.SPREAD_WARNING_DB); `None` otherwise.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.audio.loudness.voiced_lines(scene, mall)

`{voice id: [line, ...]}` for every line with audio in the store.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.audio.loudness.voiced_sources(scene, mall)

[`voiced_lines()`](_autosummary/an.audio.loudness.html.md#an.audio.loudness.voiced_lines), by each line’s SOURCE audio (a leveled line’s own).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]


# _autosummary/an.audio.mac_say_tts.html.md

# an.audio.mac_say_tts

MacSayTTS — audible offline speech via macOS’s built-in `say` command.

Free, local, no API keys, no Python deps. macOS-only. The `say` binary
ships with every macOS install since at least 10.4. Voices are listed
by `say -v '?'` and can be passed as `voice_id`; common defaults:
`"Samantha"`, `"Daniel"`, `"Karen"`, `"Alex"`.

Output is 16-bit little-endian PCM WAV at 22050 Hz so the rest of the
audio pipeline (which prefers WAV) can read frames + duration directly.

```pycon
>>> tts = MacSayTTS()
>>> tts.name
'mac_say'
```

### Classes

| [`MacSayTTS`](_autosummary/an.audio.mac_say_tts.html.md#an.audio.mac_say_tts.MacSayTTS)(\*[, default_voice_id, ...])   | macOS `say`-backed TTSProvider.   |
|-------------------------------------------------------------------------------------------|-----------------------------------|

### Exceptions

| [`MacSayTTSError`](_autosummary/an.audio.mac_say_tts.html.md#an.audio.mac_say_tts.MacSayTTSError)   | Raised when the `say` subprocess fails.   |
|-------------------------------------------------------------------|-------------------------------------------|

### *class* an.audio.mac_say_tts.MacSayTTS(, default_voice_id='Samantha', sample_rate=22050, rate_wpm=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

macOS `say`-backed TTSProvider.

Implements the `TTSProvider` protocol. Audible, deterministic, and
fully offline — uses Apple’s voice synthesis bundled with the OS.

#### billed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= False*

an#311).

* **Type:**
  Costs nothing per request (stated, never inferred

#### repeatable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

The same request gives the same audio, so best-of-N takes never apply
([`an.audio.takes.voice_takes()`](_autosummary/an.audio.takes.html.md#an.audio.takes.voice_takes)) and nothing is billed.

### *exception* an.audio.mac_say_tts.MacSayTTSError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when the `say` subprocess fails.


# _autosummary/an.audio.offline_tts.html.md

# an.audio.offline_tts

OfflineTTS — produces silent audio of plausible duration. No network, no keys.

This is the **default** TTS provider for `an`. It’s honest about being silent
(it doesn’t pretend to speak), but it produces a real WAV file with the right
duration so the rest of the pipeline (lip-sync, rendering, mp4 muxing) runs
end-to-end without any API setup.

Use `ElevenLabsTTS` for real speech once you’ve set `ELEVEN_API_KEY`.

```pycon
>>> tts = OfflineTTS()
>>> clip = tts.synthesize("Hello, world!", voice_id="default")
>>> clip.duration > 0.0
True
>>> clip.transcript
'Hello, world!'
```

### Functions

| [`estimate_speech_duration`](_autosummary/an.audio.offline_tts.html.md#an.audio.offline_tts.estimate_speech_duration)(text, \*[, ...])   | Seconds the offline voice takes to say `text` — a leading pad plus a per-character rate over the non-space characters, clamped.   |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|

### Classes

| [`OfflineTTS`](_autosummary/an.audio.offline_tts.html.md#an.audio.offline_tts.OfflineTTS)(\*[, sample_rate, channels, ...])   | Default TTS provider: silent WAV of length proportional to text.   |
|-------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|

### *class* an.audio.offline_tts.OfflineTTS(, sample_rate=22050, channels=1, seconds_per_char=0.06)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Default TTS provider: silent WAV of length proportional to text.

Implements the `TTSProvider` protocol.

#### billed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= False*

an#311).

* **Type:**
  Costs nothing per request (stated, never inferred

#### repeatable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

The same request gives the same audio, so best-of-N takes never apply
([`an.audio.takes.voice_takes()`](_autosummary/an.audio.takes.html.md#an.audio.takes.voice_takes)) and nothing is billed.

#### silent *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

a line it speaks for a voice written for a real
provider is a stand-in the render says so loudly (an#305).

* **Type:**
  Its audio is silence

### an.audio.offline_tts.estimate_speech_duration(text, , seconds_per_char=0.06)

Seconds the offline voice takes to say `text` — a leading pad plus a
per-character rate over the non-space characters, clamped.

It is exactly what an offline render gives a line (before a voice’s
`tempo`, which `an validate` divides it by), so `an validate` uses
it to warn about a shot too short for its dialogue BEFORE anything is
synthesized. A real voice is usually a little slower, so for one this is an
under-estimate: a line it says overruns will overrun.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> round(estimate_speech_duration("It only takes exact change."), 3)
1.43
```


# _autosummary/an.audio.pipeline.html.md

# an.audio.pipeline

Audio pipeline orchestration: dialogue → audio → visemes → IR mutation.

Phase 3 wires the pieces together. `produce_audio_for_scene` walks every
`Dialogue` in the scene, synthesizes its audio + viseme track via the
configured providers, persists artifacts to `mall["audio"]` /
`mall["visemes"]`, and stamps the resulting `VisemeTrack` and timing
back onto the `Dialogue` line so renderers can find it.

**Who speaks a line** (an#305): with no `tts` given, each line’s voice
document decides — its `provider` ([`an.audio.voices.declared_provider()`](_autosummary/an.audio.voices.html.md#an.audio.voices.declared_provider))
— and a voice that names none is spoken by the offline provider (silent WAV;
lip-sync defaults to deterministic offline visemes), so a project that declares
no provider runs without API keys or external binaries, exactly as before. A
`tts` given is an override for every line; a line spoken by another provider
than its voice declares is a [`VoiceStandInWarning`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VoiceStandInWarning) (an error under
`strict`). See [`tts_chooser()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.tts_chooser).

```pycon
>>> from an.audio.pipeline import default_tts, default_lipsync
>>> default_tts().name
'offline'
```

### Module Attributes

| [`VOICE_TTS`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VOICE_TTS)                   | The `tts` that means "each line's voice document names its provider" — the default of a render (an#305).    |
|------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| [`DEFAULT_TTS_NAME`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.DEFAULT_TTS_NAME)            | Who speaks a line whose voice declares no provider, when no `tts` overrides it.                             |
| [`TakeScorerFactory`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.TakeScorerFactory)           | `(TakesSpec) -> TakeScorer` — the seam that turns a takes spec into its scorer.                             |
| [`OVERRUN_TOLERANCE_S`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.OVERRUN_TOLERANCE_S)         | a frame at 60 fps (the same as `an validate`'s).                                                            |
| [`AUDIO_OUTPUT_STORES`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.AUDIO_OUTPUT_STORES)         | The stores the audio pipeline writes what it makes to.                                                      |
| [`REROLL_ONLY_HINT`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.REROLL_ONLY_HINT)            | only a new roll (new keys) replaces it.                                                                     |
| [`LEGACY_DURATION_TOLERANCE_S`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.LEGACY_DURATION_TOLERANCE_S) | How far a legacy sidecar's duration may sit from its audio's and still be the same take: a frame at 60 fps. |

### Functions

| [`audio_key`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.audio_key)(text, voice_id, tts_name[, ...])        | Content key of a line's audio: text, voice, provider, and — only when the voice declares them — its effects, the provider voice it names (an#194), the provider's synthesis options (model, settings, seed, audio tags — an#209).                                                      |
|----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`default_lipsync`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.default_lipsync)()                                 | The default lip-sync provider: the loaded genre's `offline` one (the deterministic char-to-viseme provider of `cutan`), else [`NullLipSync`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.NullLipSync).                                                                |
| [`default_tts`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.default_tts)()                                     | The default TTS provider: `OfflineTTS`.                                                                                                                                                                                                                                                |
| [`dialogue_overruns`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.dialogue_overruns)(scene, \*[, tolerance_s, mall]) | One message per synthesized line that ends past its shot's end.                                                                                                                                                                                                                        |
| [`free_and_repeatable`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.free_and_repeatable)(provider)                     | Whether `provider` may be run where nothing may be spent or written (an#311): it DECLARES `repeatable = True` (the same request gives the same bytes: `offline`, `mac_say`) and `billed = False` — stated, never inferred from a missing method — and has no `billed_characters` hook. |
| [`in_memory_audio_mall`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.in_memory_audio_mall)(mall)                        | `mall` with its audio and visemes stores behind [`InMemoryOverlay`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.InMemoryOverlay) s — hand it to [`stamp_from_stores()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.stamp_from_stores) (`free_in_memory=True`) and then to whatever keys the stamped scene (an#311).     |
| [`is_voice_tts`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.is_voice_tts)(tts)                                 | Whether `tts` means "each voice's own provider": `None`, `""` or [`VOICE_TTS`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VOICE_TTS).                                                                                                                                                           |
| [`produce_audio_for_dialogue`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.produce_audio_for_dialogue)(dialogue[, mall, ...]) | Synthesize audio + visemes for one dialogue line.                                                                                                                                                                                                                                      |
| [`produce_audio_for_scene`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.produce_audio_for_scene)(scene[, mall, tts, ...])  | Walk every dialogue line, synthesize, and stamp viseme tracks back.                                                                                                                                                                                                                    |
| [`retake_lines`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.retake_lines)(scene, mall, match, \*, tts[, ...])  | Mark the recorded takes of the lines whose text contains `match` to be chosen again on the next render; one message per matching line.                                                                                                                                                 |
| [`retime_dialogue`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.retime_dialogue)(scene, \*[, timed_shots_only])    | Stamp every synthesized line's `start` from its `pause` / `at`.                                                                                                                                                                                                                        |
| [`stamp_from_stores`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.stamp_from_stores)(scene, mall, \*[, tts, ...])    | Stamp `scene`'s dialogue exactly as [`produce_audio_for_scene()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.produce_audio_for_scene) would with these providers — `tts` as it takes it: `None` for each voice's own (an#305) — from the content-keyed `audio` and `visemes` stores only.                     |
| [`synthesis_options`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.synthesis_options)(tts, line, mall, voice_id)      | The provider-specific `synthesize` kwargs for `line` in `voice_id`.                                                                                                                                                                                                                    |
| [`takes_cost_message`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.takes_cost_message)(lines, tts, audio_store)       | What synthesizing `lines` with `tts` will bill, when any of them takes best-of-N or `tts` bills per character (an#305: a voice that names a paid provider is spoken by it with no flag, so its cost is said first); else `""`.                                                         |
| [`tts_chooser`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.tts_chooser)(tts, mall, \*[, make])                | `voice_id -> provider`: who speaks a line in that voice (an#305).                                                                                                                                                                                                                      |
| [`viseme_key`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.viseme_key)(audio_key_, lipsync_name, transcript)  | Content key of a line's viseme track (a function of the audio HEARD).                                                                                                                                                                                                                  |
| [`voice_stand_ins`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.voice_stand_ins)(lines, mall, \*, overridden)      | One message per voice whose lines are spoken by another provider than its document declares; `lines` is `(voice_id, provider)` per line, and `overridden` says whether a `tts` was given for every line.                                                                               |

### Classes

| [`InMemoryOverlay`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.InMemoryOverlay)(store)   | A store view that reads through and keeps every write IN MEMORY: what a reader of a render's cache keys stamps free audio into (an#311), so the keys it computes next see that audio while the store itself is untouched.   |
|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### Exceptions

| [`AudioNotCachedError`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.AudioNotCachedError)    | A line's audio (or its visemes) is not in the content-keyed stores, so stamping it would need a synthesis this caller does not allow.           |
|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| [`AudioPipelineError`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.AudioPipelineError)     | The scene declares audio the pipeline cannot produce.                                                                                           |
| [`DialogueOverrunWarning`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.DialogueOverrunWarning) | A synthesized line runs past its shot's end, so its tail is cut.                                                                                |
| [`TakeDigestWarning`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.TakeDigestWarning)      | The audio restored for a line's recorded take is not the audio the record names.                                                                |
| [`VoiceStandInError`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VoiceStandInError)      | Under `strict`: a line would be spoken by another provider than its voice declares.                                                             |
| [`VoiceStandInWarning`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VoiceStandInWarning)    | A line is spoken by another provider than its voice document declares — an override for every line, or a provider `an` has no TTS for (an#305). |

### an.audio.pipeline.AUDIO_OUTPUT_STORES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('audio', 'visemes')*

The stores the audio pipeline writes what it makes to.

### *exception* an.audio.pipeline.AudioNotCachedError

Bases: [`AudioPipelineError`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.AudioPipelineError)

A line’s audio (or its visemes) is not in the content-keyed stores, so
stamping it would need a synthesis this caller does not allow.

### *exception* an.audio.pipeline.AudioPipelineError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The scene declares audio the pipeline cannot produce. Carries detail.

### an.audio.pipeline.DEFAULT_TTS_NAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'offline'*

Who speaks a line whose voice declares no provider, when no `tts` overrides it.

### *exception* an.audio.pipeline.DialogueOverrunWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A synthesized line runs past its shot’s end, so its tail is cut.

### *class* an.audio.pipeline.InMemoryOverlay(store)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

A store view that reads through and keeps every write IN MEMORY: what a
reader of a render’s cache keys stamps free audio into (an#311), so the
keys it computes next see that audio while the store itself is untouched.
Deleting is refused.

### an.audio.pipeline.LEGACY_DURATION_TOLERANCE_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.016666666666666666*

How far a legacy sidecar’s duration may sit from its audio’s and still be
the same take: a frame at 60 fps.

### an.audio.pipeline.OVERRUN_TOLERANCE_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.016666666666666666*

a frame at 60 fps
(the same as `an validate`’s).

* **Type:**
  Slack before a line counts as running past its shot

### an.audio.pipeline.REROLL_ONLY_HINT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'Re-roll it with \`an voices reroll <project> <words of the line>\` (new takes under new keys, billed; the render prints the cost first)'*

only a new roll (new keys) replaces it.

* **Type:**
  The remedy when a recorded take is gone

### *exception* an.audio.pipeline.TakeDigestWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

The audio restored for a line’s recorded take is not the audio the record names.

### an.audio.pipeline.TakeScorerFactory

`(TakesSpec) -> TakeScorer` — the seam that turns a takes spec into its scorer.

alias of [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`TakesSpec`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakesSpec)], [`TakeScorer`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeScorer)]

### an.audio.pipeline.VOICE_TTS *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'voice'*

The `tts` that means “each line’s voice document names its provider” — the
default of a render (an#305). `None` and `""` mean the same.

### *exception* an.audio.pipeline.VoiceStandInError

Bases: [`AudioPipelineError`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.AudioPipelineError)

Under `strict`: a line would be spoken by another provider than its
voice declares. Raised before any request.

### *exception* an.audio.pipeline.VoiceStandInWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A line is spoken by another provider than its voice document declares —
an override for every line, or a provider `an` has no TTS for (an#305).

### an.audio.pipeline.audio_key(text, voice_id, tts_name, effects=None, , provider_voice=None, options=None, takes=None, take=None, roll=None)

Content key of a line’s audio: text, voice, provider, and — only when the
voice declares them — its effects, the provider voice it names (an#194),
the provider’s synthesis options (model, settings, seed, audio tags —
an#209). With none of them, the payload is exactly the pre-effects one, so
every key a project already has is unchanged. `take` (1, 2, … — never 0)
and `roll` (1, 2, … after `an voices reroll`) key one candidate take of
a best-of-N line; take 0 of roll 0 is the single-take request and keeps its
key. `takes` ([`an.audio.takes.takes_choice_part()`](_autosummary/an.audio.takes.html.md#an.audio.takes.takes_choice_part)) keys the CHOICE
among a line’s takes — the record in `mall["takes"]`, not an audio blob.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> audio_key("hi", "default", "offline") == audio_key(
...     "hi", "default", "offline", {}, provider_voice=None, options={},
...     takes=None, take=0, roll=0)
True
```

### an.audio.pipeline.default_lipsync()

The default lip-sync provider: the loaded genre’s `offline` one (the
deterministic char-to-viseme provider of `cutan`), else
[`NullLipSync`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.NullLipSync).

* **Return type:**
  [`LipSyncProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.LipSyncProvider)

```pycon
>>> default_lipsync().name in {"offline", "none"}
True
```

### an.audio.pipeline.default_tts()

The default TTS provider: `OfflineTTS`.

* **Return type:**
  [`TTSProvider`](_autosummary/an.audio.tts.html.md#an.audio.tts.TTSProvider)

### an.audio.pipeline.dialogue_overruns(scene, , tolerance_s=0.016666666666666666, mall=None)

One message per synthesized line that ends past its shot’s end.

`an validate` warns before synthesis from an estimate; this is the exact
check AFTER it — a voice’s `tempo` (or a real voice’s own pace, or the
silence it pads a line with) can make a line longer than estimated, and the
render cuts the shot’s audio at the shot’s end, so the tail would otherwise
be lost silently. It is `an validate`’s own check
([`an.ir.validate.shot_dialogue_overruns()`](_autosummary/an.ir.validate.html.md#an.ir.validate.shot_dialogue_overruns)), over the synthesized lines;
`mall` lets its fix name the voice’s `trim_silence`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from an.ir.schema import Dialogue, SceneIR, Shot
>>> shot = Shot(id="s", duration=1.0, dialogue=[
...     Dialogue(speaker="a", text="hi", start=0.2, duration=1.3, audio_ref="k")])
>>> dialogue_overruns(SceneIR(timeline=[shot]))[0][:46]
"shot 's': line 0 (a) ends at 1.30s as synthesi"
```

### an.audio.pipeline.free_and_repeatable(provider)

Whether `provider` may be run where nothing may be spent or written
(an#311): it DECLARES `repeatable = True` (the same request gives the
same bytes: `offline`, `mac_say`) and `billed = False` — stated, never
inferred from a missing method — and has no `billed_characters` hook.
Re-running it reproduces exactly what a render stored; anything else would
give new bytes, so new keys, or cost money.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> from an.audio.offline_tts import OfflineTTS
>>> free_and_repeatable(OfflineTTS()), free_and_repeatable(object())
(True, False)
```

### an.audio.pipeline.in_memory_audio_mall(mall)

`mall` with its audio and visemes stores behind [`InMemoryOverlay`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.InMemoryOverlay)
s — hand it to [`stamp_from_stores()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.stamp_from_stores) (`free_in_memory=True`) and then
to whatever keys the stamped scene (an#311).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.audio.pipeline.is_voice_tts(tts)

Whether `tts` means “each voice’s own provider”: `None`, `""` or
[`VOICE_TTS`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VOICE_TTS).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> [is_voice_tts(t) for t in (None, "", "voice", " Voice ", "offline")]
[True, True, True, True, False]
```

### an.audio.pipeline.produce_audio_for_dialogue(dialogue, mall=None, \*, tts=None, lipsync=None, effects=None, voice_id=None, takes=<object object>, take_scorer=<function make_take_scorer>)

Synthesize audio + visemes for one dialogue line.

Side effects: when `mall` is provided, persists the WAV to
`mall["audio"]` keyed by the content-hash of the dialogue, and persists
the viseme JSON to `mall["visemes"]` similarly. Cache-friendly: a
second call with identical inputs returns the cached versions.

`effects` (default: what the line’s voice declares in `mall["voices"]`)
is applied to the synthesized audio BEFORE alignment, so the visemes are
computed on the audio the viewer hears. The raw synthesis stays cached under
its own key, so changing an effect never re-pays the TTS.

`tts` (default: the provider the voice document declares, else offline —
[`tts_chooser()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.tts_chooser)) is an override when given.

`voice_id` (default: the line’s `voice_ref`, else `"default"`) is the
`voices`-store key; [`produce_audio_for_scene()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.produce_audio_for_scene) passes the one
[`an.audio.voices.line_voice_id()`](_autosummary/an.audio.voices.html.md#an.audio.voices.line_voice_id) resolves, so a character’s bound
voice reaches here (an#194). The provider is handed the voice document’s
own voice id when it names one.

What the provider’s optional `synthesis_options` hook derives from the
voice document and the line (ElevenLabs: `model_id`, `voice_settings`,
`seed`, and the `[emotion]`/`direction` audio tags — an#209) is passed
to `synthesize` and keyed; the text handed to alignment is always the
bare `dialogue.text`, never the tagged one.

`takes` (default: what the voice declares for this line — see
[`an.audio.takes`](_autosummary/an.audio.takes.html.md#module-an.audio.takes); `None` forces one take) synthesizes several takes,
scores each with `take_scorer(spec)` on the audio the viewer hears, keeps
the best and records the choice in `mall["takes"]`. A recorded choice is
restored from the record and never re-rolled; a recorded take whose audio is
gone raises [`TakeLostError`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeLostError) before any request.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`AudioClip`](_autosummary/an.audio.tts.html.md#an.audio.tts.AudioClip), [`VisemeTrack`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.VisemeTrack)]

### an.audio.pipeline.produce_audio_for_scene(scene, mall=None, \*, tts=None, lipsync=None, take_scorer=<function make_take_scorer>, announce=<function \_announce_to_stderr>, overruns=True, strict=False, tts_factory=None)

Walk every dialogue line, synthesize, and stamp viseme tracks back.

Mutates the `scene` in place AND returns it (for chaining).
Stamps `Dialogue.duration`, `Dialogue.viseme_track`, and
`Dialogue.audio_ref` (mall[“audio”] key) so the renderer can find the
audio later. Lines with an existing viseme_track AND audio_ref are not
re-synthesized (idempotent).

`Dialogue.start` is DERIVED on every pass, synthesized or not, by
`Dialogue.planned_start()`: the line’s `at` if set, else the previous
line’s end plus its `pause` (an#187). So editing a pause re-times the
shot without touching the audio, and every consumer of `start` — the
mux, the visemes, captions, ducking — follows. A `start` on a line that
was never synthesized is an authored start from before `at` existed,
and is kept as the line’s `at`.

Every line’s request is resolved BEFORE anything is synthesized, so a
malformed voice (an effect, a `takes`), a corrupt takes record or a
recorded take whose audio is gone fails before a credit is spent; and
`announce` (default: a line on stderr; `None` for silence) is told, before the first request, what best-of-N takes will bill (requests
and the provider’s characters) and which recorded takes were chosen by an
older scorer version than the current one (they are kept). After synthesis,
a line that ends past its shot’s end ([`dialogue_overruns()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.dialogue_overruns)) is
announced too — or, with `announce=None`, a `DialogueOverrunWarning` —
unless `overruns=False`: `an render` passes that, because it reports
every post-synthesis finding together in its summary (an#254).

**Who speaks each line** (an#305, [`tts_chooser()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.tts_chooser)): with `tts` not
given (`None`, `""` or [`VOICE_TTS`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VOICE_TTS)), the line’s voice document’s
`provider`, built by `tts_factory` (default
[`an.audio.providers.make_tts()`](_autosummary/an.audio.providers.html.md#an.audio.providers.make_tts)), else offline; a `tts` given speaks
every line. A line spoken by another provider than its voice declares is a
[`VoiceStandInWarning`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VoiceStandInWarning) — the silent offline voice says so — and,
with `strict`, a [`VoiceStandInError`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VoiceStandInError) before any request. A provider
with requests to send is asked `check_available()` first (ElevenLabs: is
there a key?), and what a provider that bills per character will bill is
announced before the first request — whatever chose the provider; a cached
line is never billed and never counted.

* **Return type:**
  [`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR)

### an.audio.pipeline.retake_lines(scene, mall, match, \*, tts, rescore=False, take_scorer=<function make_take_scorer>)

Mark the recorded takes of the lines whose text contains `match` to be
chosen again on the next render; one message per matching line.

`rescore=True` re-chooses among the takes already synthesized (with the
current scorer; nothing billed while they are cached); otherwise a new roll
of takes is synthesized (billed — the render prints the cost first). Either
way the replaced choice is kept in the record’s `history`, and the new
take has its own key, so its visemes and word timings are aligned afresh.
This is the only way a recorded take is replaced (ADR 0003).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.audio.pipeline.retime_dialogue(scene, , timed_shots_only=False)

Stamp every synthesized line’s `start` from its `pause` / `at`.

Pure — synthesizes nothing, reads no store — and idempotent. The audio
pipeline ends with it, and every path that skips the pipeline (`render`
with `auto_audio=False`, `an preview`) runs it too, so a pause edited
after synthesis is never played at the stale stamp (an#187). A shot is
re-timed up to its first line with no `duration` (never synthesized:
nothing after it has a known start). Mutates in place and returns
`scene`.

`timed_shots_only=True` — what the paths that skip synthesis pass —
leaves a shot whose lines carry no `pause`/`at` exactly as stamped:
a hand-built scene may stamp `start` itself (a test, a fixture), and
without the pipeline there is no authority to say that stamp is stale.

* **Return type:**
  [`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR)

```pycon
>>> from an.ir.schema import Dialogue, SceneIR, Shot
>>> shot = Shot(id="s", dialogue=[
...     Dialogue(speaker="a", text="hi", start=0.0, duration=0.5, audio_ref="k1"),
...     Dialogue(speaker="b", text="bye", start=0.5, duration=0.4, audio_ref="k2",
...              pause=1.5)])
>>> [d.start for d in retime_dialogue(SceneIR(timeline=[shot])).timeline[0].dialogue]
[0.0, 2.0]
```

### an.audio.pipeline.stamp_from_stores(scene, mall, , tts=None, lipsync, tts_factory=None, free_in_memory=False)

Stamp `scene`’s dialogue exactly as [`produce_audio_for_scene()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.produce_audio_for_scene)
would with these providers — `tts` as it takes it: `None` for each
voice’s own (an#305) — from the content-keyed `audio` and `visemes`
stores only. Synthesises, aligns, writes and announces nothing; a line the
stores cannot answer raises [`AudioNotCachedError`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.AudioNotCachedError).

`free_in_memory` (an#311): a line the stores cannot answer whose
provider is free and repeatable ([`free_and_repeatable()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.free_and_repeatable): offline
speech) is synthesised and aligned for real — it reproduces the bytes a
render stored — into the mall’s [`InMemoryOverlay`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.InMemoryOverlay) s, which
[`in_memory_audio_mall()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.in_memory_audio_mall) puts in front of the audio and visemes stores
(required: nothing is ever written to a store). Billed or non-repeatable
providers still raise. Still nothing is announced.

What a reader of the render’s cache keys needs (an#274): a `scene.md`
edit drops every stamp on re-sync, and the next render re-stamps the same
audio from the stores, so the keys a render WILL use are these, not the
unstamped IR’s. Mutates `scene` in place and returns it.

* **Return type:**
  [`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR)

### an.audio.pipeline.synthesis_options(tts, line, mall, voice_id)

The provider-specific `synthesize` kwargs for `line` in `voice_id`.

`{}` for a provider without a `synthesis_options` hook (offline,
mac_say) and for a voice written for another provider — which is what
keeps their cache keys where they were.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.audio.pipeline.takes_cost_message(lines, tts, audio_store)

What synthesizing `lines` with `tts` will bill, when any of them takes
best-of-N or `tts` bills per character (an#305: a voice that names a paid
provider is spoken by it with no flag, so its cost is said first); else `""`.

Counts only the requests not already in `audio_store` (a cached take is
free; a recorded take is restored, never billed), and the provider’s billed
characters per request (its `billed_characters(text, **options)` hook —
ElevenLabs counts the audio tags too). A provider without the hook bills no
characters, and the message says so.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.audio.pipeline.tts_chooser(tts, mall, , make=None)

`voice_id -> provider`: who speaks a line in that voice (an#305).

A `tts` given (a provider, or a name `make` builds) speaks every line —
an override. Otherwise (`None`, `""`, [`VOICE_TTS`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.VOICE_TTS)) the voice
document’s `provider` decides, and a voice that declares none — or one
`make` has no provider for ([`voice_stand_ins()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.voice_stand_ins) reports it) — is
spoken by [`DEFAULT_TTS_NAME`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.DEFAULT_TTS_NAME). One instance per provider name.
`make` defaults to [`an.audio.providers.make_tts()`](_autosummary/an.audio.providers.html.md#an.audio.providers.make_tts).

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`TTSProvider`](_autosummary/an.audio.tts.html.md#an.audio.tts.TTSProvider)]

```pycon
>>> mall = {"voices": {"bob": {"provider": "mac_say"}, "ann": {}}}
>>> choose = tts_chooser(None, mall)
>>> choose("bob").name, choose("ann").name, choose("nobody").name
('mac_say', 'offline', 'offline')
>>> tts_chooser("offline", mall)("bob").name
'offline'
```

### an.audio.pipeline.viseme_key(audio_key_, lipsync_name, transcript)

Content key of a line’s viseme track (a function of the audio HEARD).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.audio.pipeline.voice_stand_ins(lines, mall, , overridden)

One message per voice whose lines are spoken by another provider than its
document declares; `lines` is `(voice_id, provider)` per line, and
`overridden` says whether a `tts` was given for every line.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from an.audio.offline_tts import OfflineTTS
>>> mall = {"voices": {"bob": {"provider": "elevenlabs"}}}
>>> print(voice_stand_ins([("bob", OfflineTTS())] * 2, mall, overridden=True)[0])
voice 'bob' declares provider 'elevenlabs', but its 2 line(s) are spoken by 'offline' — SILENT audio — because `tts` overrides every voice. Drop `--tts` to let each voice speak with its own provider.
>>> voice_stand_ins([("bob", OfflineTTS())], {}, overridden=True)
[]
```


# _autosummary/an.audio.providers.html.md

# an.audio.providers

Provider factory: name → concrete TTS/LipSync provider instance.

Used by the CLI and orchestrator to map `--tts elevenlabs` (and similar)
strings into instantiated providers without callers having to import the
specific classes.

```pycon
>>> from an.audio.providers import make_tts, make_lipsync
>>> make_tts("offline").name
'offline'
>>> make_lipsync("none").name
'none'
```

### Module Attributes

| [`DEFAULT_LANGUAGE`](_autosummary/an.audio.providers.html.md#an.audio.providers.DEFAULT_LANGUAGE)   | The language a provider aligns for when the caller says nothing.     |
|---------------------------------------------------------------------|----------------------------------------------------------------------|
| [`LIPSYNC_FACTORIES`](_autosummary/an.audio.providers.html.md#an.audio.providers.LIPSYNC_FACTORIES)  | The lip-sync providers the core itself offers.                       |
| [`DFLT_LIPSYNC`](_autosummary/an.audio.providers.html.md#an.audio.providers.DFLT_LIPSYNC)       | The provider `default_lipsync()` prefers when a genre registered it. |

### Functions

| [`known_lipsync_names`](_autosummary/an.audio.providers.html.md#an.audio.providers.known_lipsync_names)()              | Return the registered LipSync provider names.                                 |
|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`known_tts_names`](_autosummary/an.audio.providers.html.md#an.audio.providers.known_tts_names)()                  | Return the registered TTS provider names.                                     |
| [`lipsync_factories`](_autosummary/an.audio.providers.html.md#an.audio.providers.lipsync_factories)()                | Every lip-sync factory by name: the core's, then the loaded genres' services. |
| [`make_lipsync`](_autosummary/an.audio.providers.html.md#an.audio.providers.make_lipsync)(name, \*[, language]) | Instantiate a LipSync provider by name.                                       |
| [`make_tts`](_autosummary/an.audio.providers.html.md#an.audio.providers.make_tts)(name)                     | Instantiate a TTS provider by name.                                           |

### an.audio.providers.DEFAULT_LANGUAGE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'en'*

The language a provider aligns for when the caller says nothing. Only
Rhubarb reads it today (its recognizer follows the language, an#96).

### an.audio.providers.DFLT_LIPSYNC *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'offline'*

The provider `default_lipsync()` prefers when a genre registered it.

### an.audio.providers.LIPSYNC_FACTORIES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[...], [LipSyncProvider](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.LipSyncProvider)]]* *= {'none': <function <lambda>>}*

The lip-sync providers the core itself offers. A genre adds its own through
the `lipsync.<name>` service (`cutan`: `offline`, `rhubarb`,
`whisper`); see [`lipsync_factories()`](_autosummary/an.audio.providers.html.md#an.audio.providers.lipsync_factories).

### an.audio.providers.known_lipsync_names()

Return the registered LipSync provider names.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.audio.providers.known_tts_names()

Return the registered TTS provider names.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.audio.providers.lipsync_factories()

Every lip-sync factory by name: the core’s, then the loaded genres’ services.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`LipSyncProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.LipSyncProvider)]]

### an.audio.providers.make_lipsync(name, , language='en')

Instantiate a LipSync provider by name.

`language` (BCP-47) reaches providers that select behaviour by language —
Rhubarb’s recognizer today; a future aligner’s weight allowlist.

The default name (`"offline"`) resolves to [`NullLipSync`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.NullLipSync)
when no loaded genre provides it, so voicing a line needs no genre; any other
name that nothing provides is an error that says how to get it.

* **Return type:**
  [`LipSyncProvider`](_autosummary/an.audio.lipsync.html.md#an.audio.lipsync.LipSyncProvider)

```pycon
>>> make_lipsync("offline").name in {"offline", "none"}
True
```

### an.audio.providers.make_tts(name)

Instantiate a TTS provider by name.

Raises `ValueError` for unknown names with a list of known options.

* **Return type:**
  [`TTSProvider`](_autosummary/an.audio.tts.html.md#an.audio.tts.TTSProvider)


# _autosummary/an.audio.takes.html.md

# an.audio.takes

Best-of-N takes: re-roll a line, score every take, keep the best, record the choice.

An expressive TTS model does not repeat itself: `eleven_v3` returned three
different files for three byte-identical requests (same text, voice, settings
and seed), and the spread between two takes of one line was larger than the
effect of any voice setting. So the way to land a delivery on its measured
targets is to synthesize a few takes and keep the one that measures closest.

A voice document opts in with `takes`:

```default
{"voice_id": "...", "model_id": "eleven_v3",
 "takes": {"n": 3,                                  # takes per line (1 = off)
           "targets": {"articulation_rate_sps": [5.2, 6.3], ...},
           "cues": {"deadpan": {"n": 4, "targets": {...}}}}}
```

- `n` — takes per line; `1` (the default) is today’s single take, byte for
  byte. A bare integer (`takes: 3`) is `{"n": 3}`.
- `targets` — `[low, high]` prosody targets ([`an.verify.prosody.METRICS`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.METRICS))
  the default scorer measures each take against. A style’s targets are named by
  role in its spec; [`style_voice_role()`](_autosummary/an.audio.takes.html.md#an.audio.takes.style_voice_role) resolves those names into values
  when the style is applied, so the voice document holds numbers, never names.
- `reference_hz` — the voice’s neutral median pitch, for a `register_st` target.
- `cues` — per-line opt-in: a line whose `{direction}` carries one of these
  cues uses that entry’s `n` / `targets` / `reference_hz` over the
  voice’s (the first matching cue, in the line’s order, wins). So a narrator
  can re-roll only its `{deadpan}` punchlines, scored as punchlines.
- `scorer` — the scorer’s name (default `prosody`); the scorer itself is a
  seam ([`TakeScorer`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeScorer), the pipeline’s `take_scorer=`).

How the pipeline uses it ([`an.audio.pipeline`](_autosummary/an.audio.pipeline.html.md#module-an.audio.pipeline)):

1. **Every take has its own content key.** Take `0` of roll `0` is the
   request a single take makes, under the same key — turning takes on for a
   line already synthesized reuses that take and pays for `n - 1` more. Take
   `i` adds `take: i` (and a re-roll adds `roll: r`) to its key, and a
   provider may vary its request per take (ElevenLabs offsets a declared
   `seed` by `i`). The audio a line KEEPS is the chosen take’s own heard
   key (its effects applied), so a different take is a different
   `audio_ref`, and its visemes, word timings and captions follow it.
2. Each take is scored on the audio the viewer hears (the voice’s effects
   applied, so a `tempo` counts toward the rate); the lowest score wins, ties
   to the lower take index.
3. **The record is the resolution** (ADR 0003 decision 3). The choice is
   written to `mall["takes"]` under the line’s *choice key* — the line’s
   request, `n`, the scorer’s name and its configuration (its targets), but
   NOT its version — with the chosen take, the sha256 of what it sounds like,
   every take’s keys, digests and scores, and the scorer that chose. Every
   render reads it first:
   - the chosen take is restored from the record (its heard audio, else its
     raw audio with the effects re-applied) and never re-billed;
   - a hand edit of `chosen` wins: that take is restored, the record says
     `superseded_by: hand` and the decision is logged;
   - a chosen take whose audio is gone is a [`TakeLostError`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeLostError) raised
     before any request — `an voices reroll` is the explicit way to re-roll;
   - a record made by an older scorer version is KEPT, and the render reports
     it — `an voices rescore` re-chooses explicitly, from the cached takes.

   Changing `n`, the targets, the scorer or the voice’s effects is a new
   choice key, chosen from the takes already cached where it can.

```pycon
>>> spec = takes_spec({"n": 3, "targets": {"f0_sd_st": [2, 4]}})
>>> spec.n, spec.scorer, dict(spec.targets)
(3, 'prosody', {'f0_sd_st': [2.0, 4.0]})
>>> takes_spec(None) is None and takes_spec(1) is None
True
>>> doc = {"n": 1, "cues": {"deadpan": {"n": 4, "targets": {"final_drop_st": [-3.6, -1.7]}}}}
>>> takes_spec(doc, direction=["deadpan"]).n, takes_spec(doc, direction=["excited"])
(4, None)
>>> takes_spec({"n": 3})
Traceback (most recent call last):
    ...
an.audio.takes.VoiceTakesError: takes: n=3 with the prosody scorer needs `targets` ([low, high] per metric) to choose by
>>> choose_take([TakeScore((0.5, 1.0)), TakeScore((0.0, 2.0)), TakeScore((0.0, 2.0))])
1
```

### Module Attributes

| [`TAKES_KEY`](_autosummary/an.audio.takes.html.md#an.audio.takes.TAKES_KEY)                       | The key, in a voice document, declaring best-of-N takes.                                                                                                                                                                                                                                  |
|----------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`TAKES_STORE`](_autosummary/an.audio.takes.html.md#an.audio.takes.TAKES_STORE)                     | The name of the store the choices are recorded in.                                                                                                                                                                                                                                        |
| [`DEFAULT_SCORER`](_autosummary/an.audio.takes.html.md#an.audio.takes.DEFAULT_SCORER)                  | The scorer a `takes` declaration uses when it names none.                                                                                                                                                                                                                                 |
| [`MAX_TAKES`](_autosummary/an.audio.takes.html.md#an.audio.takes.MAX_TAKES)                       | each one is a billed request.                                                                                                                                                                                                                                                             |
| [`TAKES_RECORD_VERSION`](_autosummary/an.audio.takes.html.md#an.audio.takes.TAKES_RECORD_VERSION)            | The shape of a record in `mall["takes"]`; raised when a field changes meaning.                                                                                                                                                                                                            |
| [`PROSODY_SCORER_VERSION`](_autosummary/an.audio.takes.html.md#an.audio.takes.PROSODY_SCORER_VERSION)          | The prosody scorer's own version (its distance and tie-break); the estimator's version ([`an.verify.prosody.ESTIMATOR_VERSION`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ESTIMATOR_VERSION)) is joined to it.                                                        |
| [`PROSODY_SCORER_AGREES_OFF_SHORT`](_autosummary/an.audio.takes.html.md#an.audio.takes.PROSODY_SCORER_AGREES_OFF_SHORT) | The prosody scorer versions whose choice the current one agrees with on a line that is NOT short ([`ProsodyTakeScorer.agrees_with()`](_autosummary/an.audio.takes.html.md#an.audio.takes.ProsodyTakeScorer.agrees_with)): version 2 changed short lines only, so a longer line's recorded pick is no "older scorer" to report. |
| [`SHORT_LINE_SYLLABLES`](_autosummary/an.audio.takes.html.md#an.audio.takes.SHORT_LINE_SYLLABLES)            | A line with fewer syllables than this ([`an.verify.prosody.count_syllables()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.count_syllables)) is SHORT: too short to show pauses or a rate (an#404, end-user test finding 8).                                            |
| [`SHORT_LINE_INAPPLICABLE`](_autosummary/an.audio.takes.html.md#an.audio.takes.SHORT_LINE_INAPPLICABLE)         | The targets a short line cannot show — they need several words — and is not scored on (recorded as `not_applicable` in the take's detail).                                                                                                                                                |
| [`SCORING_SAMPLE_RATE`](_autosummary/an.audio.takes.html.md#an.audio.takes.SCORING_SAMPLE_RATE)             | The sample rate takes are decoded at for scoring — the one the targets were measured at.                                                                                                                                                                                                  |
| [`REROLL_HINT`](_autosummary/an.audio.takes.html.md#an.audio.takes.REROLL_HINT)                     | What to run when a recorded take must be replaced (named in every error).                                                                                                                                                                                                                 |
| [`SCORERS`](_autosummary/an.audio.takes.html.md#an.audio.takes.SCORERS)                         | `TakesSpec -> TakeScorer`.                                                                                                                                                                                                                                                                |

### Functions

| [`audio_digest`](_autosummary/an.audio.takes.html.md#an.audio.takes.audio_digest)(audio)                               | The sha256 a takes record names a take's audio by.                                                                                                                                                                                |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`choose_take`](_autosummary/an.audio.takes.html.md#an.audio.takes.choose_take)(scores)                               | The index of the best take: the lowest score, ties to the lower index.                                                                                                                                                            |
| [`decode_for_scoring`](_autosummary/an.audio.takes.html.md#an.audio.takes.decode_for_scoring)(audio, \*[, sr])               | `audio` as mono float samples at `sr` Hz, decoded by ffmpeg — the decoder the targets were measured with, and the only one, so the same bytes score the same on every machine.                                                    |
| [`is_short_line`](_autosummary/an.audio.takes.html.md#an.audio.takes.is_short_line)(text)                               | Whether `text` is too short to show pauses or a rate (an#404).                                                                                                                                                                    |
| [`make_take_scorer`](_autosummary/an.audio.takes.html.md#an.audio.takes.make_take_scorer)(spec)                            | The scorer `spec` names, from [`SCORERS`](_autosummary/an.audio.takes.html.md#an.audio.takes.SCORERS) (the pipeline's default `take_scorer`).                                                                                                    |
| [`normalize_takes`](_autosummary/an.audio.takes.html.md#an.audio.takes.normalize_takes)(raw)                              | The canonical `takes` declaration of a voice document (`{}` for none).                                                                                                                                                            |
| [`read_takes_record`](_autosummary/an.audio.takes.html.md#an.audio.takes.read_takes_record)(store, key)                     | The takes record under `key` in `store` (`mall["takes"]`), or `None`.                                                                                                                                                             |
| [`require_ffmpeg`](_autosummary/an.audio.takes.html.md#an.audio.takes.require_ffmpeg)()                                  | Raise [`VoiceTakesError`](_autosummary/an.audio.takes.html.md#an.audio.takes.VoiceTakesError) with the install hint when ffmpeg is not on PATH.                                                                                                          |
| [`scorer_identity`](_autosummary/an.audio.takes.html.md#an.audio.takes.scorer_identity)(scorer)                           | The scorer as a record names it: name, version and configuration.                                                                                                                                                                 |
| [`style_voice_role`](_autosummary/an.audio.takes.html.md#an.audio.takes.style_voice_role)(spec, role)                      | The partial voice document a style casts `role` as, with target NAMES resolved.                                                                                                                                                   |
| [`takes_choice_part`](_autosummary/an.audio.takes.html.md#an.audio.takes.takes_choice_part)(scorer, n)                      | What identifies a CHOICE among `n` takes: the scorer's name and its configuration, never its version — a new scorer version keeps the takes it chose, and says so ([`REROLL_HINT`](_autosummary/an.audio.takes.html.md#an.audio.takes.REROLL_HINT)). |
| [`takes_spec`](_autosummary/an.audio.takes.html.md#an.audio.takes.takes_spec)(raw, \*[, direction])                  | The takes a line with `direction` gets from a voice's `takes`, or `None` for one take.                                                                                                                                            |
| [`voice_takes`](_autosummary/an.audio.takes.html.md#an.audio.takes.voice_takes)(mall, voice_id, \*[, direction, ...]) | The takes `mall["voices"][voice_id]` declares for a line with `direction`.                                                                                                                                                        |
| [`write_takes_record`](_autosummary/an.audio.takes.html.md#an.audio.takes.write_takes_record)(store, key, record)            | Write `record` under `key` (stable, indented JSON); no store, no record.                                                                                                                                                          |

### Classes

| [`ProsodyTakeScorer`](_autosummary/an.audio.takes.html.md#an.audio.takes.ProsodyTakeScorer)(targets, \*[, reference_hz, sr])   | Scores a take by how far its prosody sits from `[low, high]` targets.         |
|-------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`TakeScore`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeScore)(value[, detail])                           | A take's score — compared as a tuple, lower is better — and what it measured. |
| [`TakeScorer`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeScorer)(\*args, \*\*kwargs)                       | Scores one take of a line.                                                    |
| [`TakesSpec`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakesSpec)(n[, scorer, targets, reference_hz])        | The takes ONE line gets: how many, and what chooses between them.             |

### Exceptions

| [`TakeLostError`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeLostError)    | A line's recorded take is gone from the audio store and cannot be restored.   |
|-------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`TakesRecordError`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakesRecordError) | A `mall["takes"]` record cannot be read as a takes record.                    |
| [`VoiceTakesError`](_autosummary/an.audio.takes.html.md#an.audio.takes.VoiceTakesError)  | A voice's `takes` declaration is malformed, or cannot be scored.              |

### an.audio.takes.DEFAULT_SCORER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'prosody'*

The scorer a `takes` declaration uses when it names none.

### an.audio.takes.MAX_TAKES *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 10*

each one is a billed request.

* **Type:**
  More takes than this per line is refused

### an.audio.takes.PROSODY_SCORER_AGREES_OFF_SHORT *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'1'})*

The prosody scorer versions whose choice the current one agrees with on a
line that is NOT short ([`ProsodyTakeScorer.agrees_with()`](_autosummary/an.audio.takes.html.md#an.audio.takes.ProsodyTakeScorer.agrees_with)): version 2
changed short lines only, so a longer line’s recorded pick is no “older
scorer” to report.

### an.audio.takes.PROSODY_SCORER_VERSION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '2'*

The prosody scorer’s own version (its distance and tie-break); the
estimator’s version ([`an.verify.prosody.ESTIMATOR_VERSION`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ESTIMATOR_VERSION)) is joined to it.
2 (an#404): a short line is scored only on the targets it can show.

### *class* an.audio.takes.ProsodyTakeScorer(targets, , reference_hz=None, sr=16000)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Scores a take by how far its prosody sits from `[low, high]` targets.

The score is [`an.verify.prosody.target_distance()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.target_distance): the summed distance
outside the ranges, then the summed distance from their midpoints (the
tie-break between takes all on target), in units of each range’s width.

A SHORT line (fewer than [`SHORT_LINE_SYLLABLES`](_autosummary/an.audio.takes.html.md#an.audio.takes.SHORT_LINE_SYLLABLES) syllables) is scored
only on the targets it can show: the pause and rate targets
([`SHORT_LINE_INAPPLICABLE`](_autosummary/an.audio.takes.html.md#an.audio.takes.SHORT_LINE_INAPPLICABLE)) are left out and recorded as
`not_applicable` (an#404). Otherwise a one-word line’s choice measured
its shortness, every target it cannot measure costing a range-width.
That changed version 1’s choices for short lines only; a pick already
recorded keeps its take and its keys (the choice key never holds the
version), and a longer line’s pick is not reported as made by an older
scorer ([`agrees_with()`](_autosummary/an.audio.takes.html.md#an.audio.takes.ProsodyTakeScorer.agrees_with)).

#### SCORE_MEANS *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= "score = [outside, off_centre], lower is better; the take with the smallest pair is kept. outside: for each target in scorer.config.targets, how far the measured value sits outside its [low, high] range, in widths of that range (0 inside; a value this take cannot measure, listed in \`unmeasured\`, counts 1). off_centre: each value's distance from its range's middle, in the same units (the tie-break). \`measured\` holds the values, \`misses\` the targets missed. A line under 4 syllables is not scored on the targets it cannot show (pauses and rates), listed in \`not_applicable\`."*

What a take’s `score` pair means, written into each takes record (an#397).

#### agrees_with(version, text)

Whether a choice recorded by scorer `version` is the one this scorer
would make for `text`: the same version, or an older one that scored
a line like this one the same way (version 1, on a line that is not short).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> s = ProsodyTakeScorer({"f0_sd_st": [2, 4]})
>>> old = s.version.replace("2+", "1+", 1)
>>> s.agrees_with(old, "He did not do the job."), s.agrees_with(old, "Hi!")
(True, False)
```

#### applicable(text)

The targets `text` is scored on: all of them, or for a short line
those it can show (an#404).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]]

```pycon
>>> s = ProsodyTakeScorer({"f0_sd_st": [2, 4], "pause_share": [0.1, 0.2]})
>>> sorted(s.applicable("Hi!")), sorted(s.applicable("He did not do the job."))
(['f0_sd_st'], ['f0_sd_st', 'pause_share'])
```

#### check_available()

Raise before any request when this scorer could not score (no ffmpeg).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.audio.takes.REROLL_HINT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 're-roll it explicitly with \`an voices reroll <project> <words of the line>\` (new takes, billed) or, while every take of its roll is still cached, re-choose among them with \`an voices rescore <project> <words of the line>\`'*

What to run when a recorded take must be replaced (named in every error).

### an.audio.takes.SCORERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[TakesSpec](_autosummary/an.audio.takes.html.md#an.audio.takes.TakesSpec)], [TakeScorer](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeScorer)]]* *= {'prosody': <function <lambda>>}*

`TakesSpec -> TakeScorer`.

* **Type:**
  Scorer factories by name

### an.audio.takes.SCORING_SAMPLE_RATE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 16000*

The sample rate takes are decoded at for scoring — the one the targets were measured at.

### an.audio.takes.SHORT_LINE_INAPPLICABLE *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'articulation_rate_sps', 'pause_median_s', 'pause_p90_s', 'pause_share', 'pauses_per_min', 'speech_rate_sps'})*

The targets a short line cannot show — they need several words — and is not
scored on (recorded as `not_applicable` in the take’s detail).

### an.audio.takes.SHORT_LINE_SYLLABLES *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 4*

A line with fewer syllables than this ([`an.verify.prosody.count_syllables()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.count_syllables))
is SHORT: too short to show pauses or a rate (an#404, end-user test finding 8).

### an.audio.takes.TAKES_KEY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'takes'*

The key, in a voice document, declaring best-of-N takes.

### an.audio.takes.TAKES_RECORD_VERSION *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 1*

The shape of a record in `mall["takes"]`; raised when a field changes meaning.

### an.audio.takes.TAKES_STORE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'takes'*

The name of the store the choices are recorded in.

### *exception* an.audio.takes.TakeLostError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A line’s recorded take is gone from the audio store and cannot be restored.

Raised before any request: re-rolling an approved take is never implicit.

### *class* an.audio.takes.TakeScore(value, detail=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A take’s score — compared as a tuple, lower is better — and what it measured.

### *class* an.audio.takes.TakeScorer(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Scores one take of a line. Deterministic: the same bytes score the same.

`name`, `version` and `config` enter the line’s audio key, so a scorer
that would choose differently must differ in one of them.

#### score(audio, text)

Score `audio` (any container ffmpeg reads; WAV without it) speaking `text`.

* **Return type:**
  [`TakeScore`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeScore)

### *exception* an.audio.takes.TakesRecordError

Bases: [`VoiceTakesError`](_autosummary/an.audio.takes.html.md#an.audio.takes.VoiceTakesError)

A `mall["takes"]` record cannot be read as a takes record.

### *class* an.audio.takes.TakesSpec(n, scorer='prosody', targets=<factory>, reference_hz=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The takes ONE line gets: how many, and what chooses between them.

### *exception* an.audio.takes.VoiceTakesError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A voice’s `takes` declaration is malformed, or cannot be scored.

### an.audio.takes.audio_digest(audio)

The sha256 a takes record names a take’s audio by.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.audio.takes.choose_take(scores)

The index of the best take: the lowest score, ties to the lower index.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### an.audio.takes.decode_for_scoring(audio, , sr=16000)

`audio` as mono float samples at `sr` Hz, decoded by ffmpeg — the decoder
the targets were measured with, and the only one, so the same bytes score the
same on every machine. Raises [`VoiceTakesError`](_autosummary/an.audio.takes.html.md#an.audio.takes.VoiceTakesError) without ffmpeg.

### an.audio.takes.is_short_line(text)

Whether `text` is too short to show pauses or a rate (an#404).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_short_line("Hi!"), is_short_line("[deadpan] No."), is_short_line("He did not do the job.")
(True, True, False)
```

### an.audio.takes.make_take_scorer(spec)

The scorer `spec` names, from [`SCORERS`](_autosummary/an.audio.takes.html.md#an.audio.takes.SCORERS) (the pipeline’s default `take_scorer`).

* **Return type:**
  [`TakeScorer`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakeScorer)

### an.audio.takes.normalize_takes(raw)

The canonical `takes` declaration of a voice document (`{}` for none).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> normalize_takes(3)
{'n': 3}
>>> normalize_takes({"cues": {"deadpan": 4}})
{'cues': {'deadpan': {'n': 4}}}
```

### an.audio.takes.read_takes_record(store, key)

The takes record under `key` in `store` (`mall["takes"]`), or `None`.

Raises [`TakesRecordError`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakesRecordError) — naming the record and the remedy — for one
that is not JSON, not a mapping, or whose `chosen` names no take.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> read_takes_record({}, "k") is None
True
>>> read_takes_record({"k": b'{"chosen": 0, "takes": [{"take": 0}]}'}, "k")["chosen"]
0
>>> read_takes_record({"k": b"[1, 2]"}, "k")
Traceback (most recent call last):
    ...
an.audio.takes.TakesRecordError: the takes record 'k' is not a takes record (a JSON object with `chosen` and `takes`): ...
```

### an.audio.takes.require_ffmpeg()

Raise [`VoiceTakesError`](_autosummary/an.audio.takes.html.md#an.audio.takes.VoiceTakesError) with the install hint when ffmpeg is not on PATH.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.audio.takes.scorer_identity(scorer)

The scorer as a record names it: name, version and configuration.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.audio.takes.style_voice_role(spec, role)

The partial voice document a style casts `role` as, with target NAMES resolved.

A style spec (`cutan.style_spec(name)`) declares
`live.voice.roles.<role>` and may name a delivery’s targets by its key in
`prosody_targets` (`takes: {n: 3, targets: narrator}`). This returns a
copy whose `takes` (and each cue’s) hold the target VALUES, ready to merge
beside a `voice_id`: `{**style_voice_role(spec, "narrator"), "voice_id": ...}`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> spec = {"live": {"voice": {"roles": {"narrator": {"model_id": "eleven_v3",
...     "takes": {"n": 2, "targets": "narrator", "cues": {"deadpan": {"targets": "punchline"}}}}}}},
...     "prosody_targets": {"narrator": {"f0_sd_st": [3.9, 5.2]}, "punchline": {"f0_sd_st": [2.2, 3.4]}}}
>>> role = style_voice_role(spec, "narrator")
>>> role["takes"]["targets"], role["takes"]["cues"]["deadpan"]["targets"]
({'f0_sd_st': [3.9, 5.2]}, {'f0_sd_st': [2.2, 3.4]})
```

### an.audio.takes.takes_choice_part(scorer, n)

What identifies a CHOICE among `n` takes: the scorer’s name and its
configuration, never its version — a new scorer version keeps the takes it
chose, and says so ([`REROLL_HINT`](_autosummary/an.audio.takes.html.md#an.audio.takes.REROLL_HINT)).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.audio.takes.takes_spec(raw, , direction=None)

The takes a line with `direction` gets from a voice’s `takes`, or `None` for one take.

Raises [`VoiceTakesError`](_autosummary/an.audio.takes.html.md#an.audio.takes.VoiceTakesError) for a malformed declaration, and for more
than one take with nothing to choose by.

* **Return type:**
  [`TakesSpec`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakesSpec) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.audio.takes.voice_takes(mall, voice_id, , direction=None, tts_name=None, repeatable=False)

The takes `mall["voices"][voice_id]` declares for a line with `direction`.

`None` (one take) for a voice not in the store, one declaring nothing, one
written for another provider than `tts_name`, and any voice spoken by a
`repeatable` provider (`offline`, `mac_say`: the same request gives the
same audio, so a second take is the first one again). A malformed
declaration raises [`VoiceTakesError`](_autosummary/an.audio.takes.html.md#an.audio.takes.VoiceTakesError) in every case.

* **Return type:**
  [`TakesSpec`](_autosummary/an.audio.takes.html.md#an.audio.takes.TakesSpec) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.audio.takes.write_takes_record(store, key, record)

Write `record` under `key` (stable, indented JSON); no store, no record.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.audio.tts.html.md

# an.audio.tts

TTS provider protocol + supporting dataclasses.

### Classes

| [`AudioClip`](_autosummary/an.audio.tts.html.md#an.audio.tts.AudioClip)([path, bytes_, duration, ...])   | A rendered audio clip, on disk or in memory.       |
|---------------------------------------------------------------------------------------------|----------------------------------------------------|
| [`TTSProvider`](_autosummary/an.audio.tts.html.md#an.audio.tts.TTSProvider)(\*args, \*\*kwargs)            | Text-to-speech provider.                           |
| [`VoiceMeta`](_autosummary/an.audio.tts.html.md#an.audio.tts.VoiceMeta)(voice_id, name, provider[, ...]) | Metadata for a TTS voice as exposed by a provider. |

### *class* an.audio.tts.AudioClip(path=None, bytes_=None, duration=0.0, sample_rate=44100, channels=1, voice_id=None, transcript=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A rendered audio clip, on disk or in memory.

### *class* an.audio.tts.TTSProvider(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Text-to-speech provider.

#### list_voices()

Return all voices the provider exposes.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`VoiceMeta`](_autosummary/an.audio.tts.html.md#an.audio.tts.VoiceMeta)]

#### synthesize(text, voice_id, \*\*kw)

Render `text` in `voice_id`’s voice. Returns an AudioClip.

* **Return type:**
  [`AudioClip`](_autosummary/an.audio.tts.html.md#an.audio.tts.AudioClip)

### *class* an.audio.tts.VoiceMeta(voice_id, name, provider, language='en', gender=None, extra=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Metadata for a TTS voice as exposed by a provider.


# _autosummary/an.audio.voices.html.md

# an.audio.voices

Which voice speaks a dialogue line: the character → voice binding (an#194).

A line resolves its voice — a key of the project’s `voices` store — in this
order, first hit wins:

1. the line’s own `voice_ref` (set from Python or `ir/scene.json`);
2. the speaking character’s `voice_ref`: the shot entity whose `id` is the
   line’s `speaker`, read from its descriptor in the `characters` store with
   the entity’s `overrides` merged over it (so `overrides: {voice_ref: x}`
   on the entity re-voices it for one shot);
3. [`DEFAULT_VOICE`](_autosummary/an.audio.voices.html.md#an.audio.voices.DEFAULT_VOICE).

A voice document may name the TTS provider’s own voice with `voice_id` (a
`say -v` name for `mac_say`, a voice id for ElevenLabs); the provider is
handed that, and otherwise the store key itself. A document that declares
`provider` scopes its provider-specific keys (`voice_id`, and the
`model_id` / `voice_settings` / `seed` ElevenLabs reads — an#209) to that
provider: rendered with another one, the line is handed `"default"`, so an
ElevenLabs-voiced project previews with `mac_say` or `offline` instead of
failing on a foreign voice id. Nothing declared anywhere
resolves every line to `"default"` handed to the provider as `"default"` —
exactly what the pipeline did before this module, so no cache key moves.

The document’s `provider` also DECIDES who speaks the line when the render
names no provider (an#305): [`declared_provider()`](_autosummary/an.audio.voices.html.md#an.audio.voices.declared_provider) reads it, and
[`an.audio.pipeline.tts_chooser()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.tts_chooser) turns it into the provider — so a voice
written for ElevenLabs is spoken by ElevenLabs by a plain `an render`, and
`--tts` is an override for every line.

```pycon
>>> from an.ir.schema import AssetRef, Dialogue, Shot
>>> shot = Shot(id="s", entities=[
...     AssetRef(kind="character", id="carl", store="characters", ref="carl"),
...     AssetRef(kind="character", id="ned", store="characters", ref="ned",
...              overrides={"voice_ref": "ned_sad"})])
>>> mall = {"characters": {"carl": {"voice_ref": "carl_kid"}, "ned": {"voice_ref": "ned_kid"}},
...         "voices": {"carl_kid": {"voice_id": "Junior"}}}
>>> [line_voice_id(Dialogue(speaker=s, text="hi"), shot, mall) for s in ("carl", "ned", "narrator")]
['carl_kid', 'ned_sad', 'default']
>>> line_voice_id(Dialogue(speaker="carl", text="hi", voice_ref="own"), shot, mall)
'own'
>>> provider_voice(mall, "carl_kid"), provider_voice(mall, "default")
('Junior', None)
>>> mall["voices"]["bob"] = {"provider": "elevenlabs", "voice_id": "abc123"}
>>> provider_voice(mall, "bob", tts_name="elevenlabs"), provider_voice(mall, "bob", tts_name="mac_say")
('abc123', 'default')
>>> declared_provider(mall, "bob"), declared_provider(mall, "carl_kid")
('elevenlabs', None)
```

### Module Attributes

| [`DEFAULT_VOICE`](_autosummary/an.audio.voices.html.md#an.audio.voices.DEFAULT_VOICE)       | The voice a line gets when neither it nor its speaker names one.                                                     |
|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| [`PROVIDER_VOICE_KEY`](_autosummary/an.audio.voices.html.md#an.audio.voices.PROVIDER_VOICE_KEY)  | The key, in a voice document, naming the TTS provider's own voice.                                                   |
| [`PROVIDER_KEY`](_autosummary/an.audio.voices.html.md#an.audio.voices.PROVIDER_KEY)        | The key, in a voice document, naming the TTS provider it is written for.                                             |
| [`CHARACTER_VOICE_KEY`](_autosummary/an.audio.voices.html.md#an.audio.voices.CHARACTER_VOICE_KEY) | The key, in a character descriptor (or an entity's `overrides`), naming the character's voice in the `voices` store. |

### Functions

| [`declared_provider`](_autosummary/an.audio.voices.html.md#an.audio.voices.declared_provider)(mall, voice_id)              | The TTS provider `mall["voices"][voice_id]` is written for, lower-cased, or `None` when the voice declares none (or has no document).   |
|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
| [`line_voice_id`](_autosummary/an.audio.voices.html.md#an.audio.voices.line_voice_id)(line, shot, mall, \*[, default]) | The `voices`-store key `line` is spoken with (see the module doc).                                                                      |
| [`provider_voice`](_autosummary/an.audio.voices.html.md#an.audio.voices.provider_voice)(mall, voice_id, \*[, tts_name]) | The provider voice `mall["voices"][voice_id]` names, or `None`.                                                                         |
| [`speaker_voice_ref`](_autosummary/an.audio.voices.html.md#an.audio.voices.speaker_voice_ref)(speaker, shot, mall)         | The voice the speaking character is bound to in `shot`, or `None`.                                                                      |
| [`voice_applies`](_autosummary/an.audio.voices.html.md#an.audio.voices.voice_applies)(doc, tts_name)                   | Whether `doc`'s provider-specific keys apply under the TTS `tts_name`.                                                                  |
| [`voice_document`](_autosummary/an.audio.voices.html.md#an.audio.voices.voice_document)(mall, voice_id)                 | `mall["voices"][voice_id]` when it is a mapping, else `{}`.                                                                             |

### an.audio.voices.CHARACTER_VOICE_KEY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'voice_ref'*

The key, in a character descriptor (or an entity’s `overrides`), naming
the character’s voice in the `voices` store.

### an.audio.voices.DEFAULT_VOICE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'default'*

The voice a line gets when neither it nor its speaker names one.

### an.audio.voices.PROVIDER_KEY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'provider'*

The key, in a voice document, naming the TTS provider it is written for.

### an.audio.voices.PROVIDER_VOICE_KEY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'voice_id'*

The key, in a voice document, naming the TTS provider’s own voice.

### an.audio.voices.declared_provider(mall, voice_id)

The TTS provider `mall["voices"][voice_id]` is written for, lower-cased,
or `None` when the voice declares none (or has no document).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> declared_provider({"voices": {"v": {"provider": " ElevenLabs "}}}, "v")
'elevenlabs'
>>> declared_provider({"voices": {"v": {"provider": ""}}}, "v") is None
True
```

### an.audio.voices.line_voice_id(line, shot, mall, , default='default')

The `voices`-store key `line` is spoken with (see the module doc).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.audio.voices.provider_voice(mall, voice_id, , tts_name=None)

The provider voice `mall["voices"][voice_id]` names, or `None`.

`None` means “hand the provider `voice_id` itself” — a voice that is not
in the store, a document without `voice_id`, or one whose `voice_id` is
its own key (which changes nothing, so it must not move a cache key).
Given `tts_name`, a document written for ANOTHER provider gives
[`DEFAULT_VOICE`](_autosummary/an.audio.voices.html.md#an.audio.voices.DEFAULT_VOICE) — never a foreign voice id (an#209).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.audio.voices.speaker_voice_ref(speaker, shot, mall)

The voice the speaking character is bound to in `shot`, or `None`.

`None` when no character entity of the shot has the speaker’s id (an
off-screen narrator), or when neither its descriptor nor its `overrides`
name a voice. A store that is absent, or that does not hold the ref, still
lets the entity’s `overrides` speak.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.audio.voices.voice_applies(doc, tts_name)

Whether `doc`’s provider-specific keys apply under the TTS `tts_name`.

True when the document names no `provider`, or names this one (case
ignored), or when the caller does not say which provider is speaking.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> voice_applies({}, "offline"), voice_applies({"provider": "ElevenLabs"}, "elevenlabs")
(True, True)
>>> voice_applies({"provider": "elevenlabs"}, "offline")
False
```

### an.audio.voices.voice_document(mall, voice_id)

`mall["voices"][voice_id]` when it is a mapping, else `{}`.

* **Return type:**
  [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)


# _autosummary/an.base.html.md

# an.base

Core types, constants, and re-exports for an.

This module is the *type vocabulary* shared across the package: schema versions,
default render parameters, easing presets, type aliases for paths and time. Heavy
data classes (the Pydantic IR models, the Renderer/Verifier protocols) live in
their own subpackages and are re-exported from `an` itself.

Keep this module small and dependency-light. Everything here should import in a
fraction of a second so the CLI is snappy.

### Module Attributes

| [`SCHEMA_VERSION`](_autosummary/an.base.html.md#an.base.SCHEMA_VERSION)                     | Current Scene IR schema version.                                                                                                                                                                                                                                                                                |
|-------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`COMPATIBLE_VERSION`](_autosummary/an.base.html.md#an.base.COMPATIBLE_VERSION)                 | Minimum Scene IR version this code can still read without migration.                                                                                                                                                                                                                                            |
| [`DEFAULT_SUPERSAMPLE`](_autosummary/an.base.html.md#an.base.DEFAULT_SUPERSAMPLE)                | Render at this many times the declared resolution, then resolve back with an exact block mean.                                                                                                                                                                                                                  |
| [`MP4_FASTSTART_ARGS`](_autosummary/an.base.html.md#an.base.MP4_FASTSTART_ARGS)                 | Put the mp4's `moov` atom in front of `mdat`, so a player can start before the file has finished downloading.                                                                                                                                                                                                   |
| [`BT709_SCALE_FILTER`](_autosummary/an.base.html.md#an.base.BT709_SCALE_FILTER)                 | The RGB->YUV conversion `an` performs, stated EXPLICITLY rather than left to the encoder flags to imply.                                                                                                                                                                                                        |
| [`EASING_PRESETS`](_autosummary/an.base.html.md#an.base.EASING_PRESETS)                     | Named easing presets.                                                                                                                                                                                                                                                                                           |
| [`COLOUR_PROPERTY`](_autosummary/an.base.html.md#an.base.COLOUR_PROPERTY)                    | The property names the cutout runtime animates NUMERICALLY.                                                                                                                                                                                                                                                     |
| [`PLANE_REST_VALUES`](_autosummary/an.base.html.md#an.base.PLANE_REST_VALUES)                  | a node targeted with any of them is drawn on a plane tilted away from the camera (`rotation_x`, radians, positive = the top recedes; `perspective`, the eye's distance in frame heights; `plane_fade_start`/`plane_fade_end`, the far fade, scene px up the plane from the hinge, off while the end is `<= 0`). |
| [`TRIM_PROPERTIES`](_autosummary/an.base.html.md#an.base.TRIM_PROPERTIES)                    | The path-only properties inside `TRANSFORM_PROPERTIES` (an#160; the name predates `dash_offset`, an#161): trim and the dash phase.                                                                                                                                                                              |
| [`SWAP_SET_NAME_FORBIDDEN_SUBSTRINGS`](_autosummary/an.base.html.md#an.base.SWAP_SET_NAME_FORBIDDEN_SUBSTRINGS) | `/` would read as a path segment and `::` is the runtime's pose-key separator.                                                                                                                                                                                                                                  |
| [`AUTHORABLE_PROPERTIES`](_autosummary/an.base.html.md#an.base.AUTHORABLE_PROPERTIES)              | a compiled transform channel, or the authored colour spelling the compiler expands.                                                                                                                                                                                                                             |
| [`PathStr`](_autosummary/an.base.html.md#an.base.PathStr)                            | Slash-delimited node path, e.g. `"charlie/head/mouth"`.                                                                                                                                                                                                                                                         |
| [`Seconds`](_autosummary/an.base.html.md#an.base.Seconds)                            | Time in seconds.                                                                                                                                                                                                                                                                                                |
| [`EasingSpec`](_autosummary/an.base.html.md#an.base.EasingSpec)                         | Either an easing preset name or a 4-tuple cubic-Bézier control [cx1,cy1,cx2,cy2].                                                                                                                                                                                                                               |
| [`RendererName`](_autosummary/an.base.html.md#an.base.RendererName)                       | Which renderer draws a shot.                                                                                                                                                                                                                                                                                    |
| [`SUPPORTED_RENDERERS`](_autosummary/an.base.html.md#an.base.SUPPORTED_RENDERERS)                | The same vocabulary as [`RendererName`](_autosummary/an.base.html.md#an.base.RendererName), as a runtime tuple — DERIVED from it, because a hand-typed second copy is a second SSOT that drifts on the day a renderer is added and nothing fails.                                                                     |
| [`TRANSITION_KINDS`](_autosummary/an.base.html.md#an.base.TRANSITION_KINDS)                   | The transition kinds a shot may be entered by.                                                                                                                                                                                                                                                                  |
| [`WIPE_DIRECTIONS`](_autosummary/an.base.html.md#an.base.WIPE_DIRECTIONS)                    | The ways a `wipe`'s edge can travel across the frame (an#390).                                                                                                                                                                                                                                                  |
| [`DEFAULT_TRANSITION_DURATION`](_autosummary/an.base.html.md#an.base.DEFAULT_TRANSITION_DURATION)        | half a second is a conventional editor's default, short enough not to eat a line of dialogue and long enough to read as deliberate.                                                                                                                                                                             |
| [`DEFAULT_TRANSITION_COLOR`](_autosummary/an.base.html.md#an.base.DEFAULT_TRANSITION_COLOR)           | The colour a `fade` passes through when it names none.                                                                                                                                                                                                                                                          |
| [`FILM_AUDIO_SAMPLE_RATE`](_autosummary/an.base.html.md#an.base.FILM_AUDIO_SAMPLE_RATE)             | The film mix's sample rate and channel count.                                                                                                                                                                                                                                                                   |
| [`DEFAULT_DUCK_DB`](_autosummary/an.base.html.md#an.base.DEFAULT_DUCK_DB)                    | how far a ducked cue drops under dialogue, and how fast it gets there and comes back.                                                                                                                                                                                                                           |

### Functions

| [`swap_set_name_problem`](_autosummary/an.base.html.md#an.base.swap_set_name_problem)(name)   | Why `name` cannot be a swap-set name, or `None` if it can.   |
|--------------------------------------------------------------------------------|--------------------------------------------------------------|

### an.base.AUTHORABLE_PROPERTIES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'alpha', 'dash_offset', 'perspective', 'pivot_x', 'pivot_y', 'plane_fade_end', 'plane_fade_start', 'rotation', 'rotation_rad', 'rotation_x', 'scale_x', 'scale_y', 'skew_x', 'skew_y', 'tint', 'tint_b', 'tint_g', 'tint_r', 'trim_end', 'trim_start', 'x', 'y'})*

a compiled transform channel, or the authored
colour spelling the compiler expands. Validate checks against this, and the
compiler’s rest table against `TRANSFORM_PROPERTIES` — the difference between
the two sets is exactly `tint`, and a test pins that rather than trusting it.

* **Type:**
  What a set/tween may name

### an.base.BT709_SCALE_FILTER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'scale=out_range=tv:out_color_matrix=bt709'*

The RGB->YUV conversion `an` performs, stated EXPLICITLY rather than left to
the encoder flags to imply.

an#34 pinned `-colorspace bt709` and measured that it does not merely *tag*
the file: it sets the matrix of the auto-inserted conversion, so the encoded
planes themselves change. That measurement was made on ffmpeg 8/9 and \*\*is
not a universal ffmpeg fact\*\* (an#148). Measured on the product’s own
`_ffmpeg_mux` with flat known-colour frames, decoded as raw `yuv420p` (planes
verbatim, no conversion on read), against the analytic limited-range values:

build | pure red decodes to Y | BT.601 says | BT.709 says |
<br/>

```
|---|
```

—

```
|---|
```

—|
| ffmpeg 9.0.1 | 63 | 81.5 | 62.6 |
| ffmpeg 6.1.6 | 81 | 81.5 | 62.6 |

Over five colours and all three planes, the worst disagreement is 0.44 from
BT.709 and 28.45 from BT.601 on ffmpeg 9 – and 0.48 from BT.601 against
27.63 from BT.709 on ffmpeg 6.1.6, which is the CI runner’s apt build. Both
files are *tagged* `color_space=bt709 color_primaries=bt709
color_transfer=bt709 color_range=tv`. So on 6.1 an#34’s re-baseline was
metadata only: `an` shipped BT.601 planes wearing a BT.709 label, and a
conforming player undoes a conversion that was never applied.

This filter removes the build-dependence by naming the conversion instead of
inferring it. It is BYTE-IDENTICAL to today’s output on ffmpeg 9.0.1
(measured: same mp4 sha256 on real corpus frames), so it re-baselines nothing
where the flags already worked, and it moves ffmpeg 6.1.6’s output onto the
same conversion (its residual against an analytic BT.709 reference drops from
luma mean 0.788 to 0.205 – ffmpeg 9’s own number to three decimals).

\*\*The committed ledger rows were captured on ffmpeg 8.1, which is not on this
machine, so the byte-identity above was not re-measured there.\*\* That those
rows are equally unaffected rests on an#34’s own measurement — that on 8.x
`-colorspace bt709` already set the auto-inserted conversion, so this filter
names what that build was doing anyway. It is a sound inference from a
recorded measurement, not a second observation; treat it as such if an 8.x
build ever contradicts a row.

Here, and not beside one of the ffmpeg calls, for the same reason
`MP4_FASTSTART_ARGS` is: **three commands must agree on it**, and they are in
two packages. The delivered mux (`_ffmpeg_mux`), the bench’s lossless
reference leg and the bench’s PNG decode leg all perform this conversion, and
the lossless leg’s entire contract is “these ARE the planes libx264
received”. Pin one and not the others and every encode-side metric silently
measures a colour-space disagreement instead of encoder damage – the failure
`an/bench/imageio.py`’s module docstring records CI catching once already.

### an.base.COLOUR_PROPERTY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'tint'*

The property names the cutout runtime animates NUMERICALLY. Any other
property on a set/tween names a swap SET declared by the target entity’s
descriptor (an#87). This is the SSOT the three consumers share so the IR
validator, the character validator and the compiler cannot drift: the
compiler derives its rest-value table from `TransformJSON` and a test
asserts that derivation equals this set. Lives here because `an.base` is
the one module all three layers may import.
The colour multiply as an AUTHOR spells it. Not in
`TRANSFORM_PROPERTIES` because nothing downstream of the compiler ever
sees it — `_expand_tint_actions` rewrites each leaf into the three numeric
components before the swap-set dispatch, which would otherwise read it as an
asset-set name (an#62).

### an.base.COMPATIBLE_VERSION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '0.3.0'*

Minimum Scene IR version this code can still read without migration.

### an.base.DEFAULT_DUCK_DB *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= -12.0*

how far a ducked cue drops under dialogue, and how fast
it gets there and comes back. The ramps are linear in gain.

* **Type:**
  Ducking defaults

### an.base.DEFAULT_SUPERSAMPLE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 1*

Render at this many times the declared resolution, then resolve back with an
exact block mean. **1 is off, and off is free**: the un-supersampled path
keeps Chromium’s own PNG bytes and pays nothing.

Here rather than in `an.adapters.cutout.supersample`, where the rest of the
mechanism lives, because `an/adapters/_base.py` needs it for
`RenderContext`’s default and importing the cutout package from there is a
real cycle (`_base` -> `cutout` -> `render` -> `_base`), not a hypothetical
one. `an/base.py` imports nothing from `an`.

The default stays 1 deliberately. Supersampling ships OPT-IN with its A/B
committed (an#58, discussion #52), per the standing rule that a default
chosen by taste ships opt-in and the flip is its own one-line change.

### an.base.DEFAULT_TRANSITION_COLOR *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '#000000'*

The colour a `fade` passes through when it names none.

### an.base.DEFAULT_TRANSITION_DURATION *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.5*

half a second
is a conventional editor’s default, short enough not to eat a line of
dialogue and long enough to read as deliberate.

* **Type:**
  Seconds, when a `fade` or `dissolve` names no duration

### an.base.EASING_PRESETS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('linear', 'ease', 'ease_in', 'ease_out', 'ease_in_out', 'step')*

Named easing presets. Renderers should accept these and the cubic-Bézier
4-tuple form `[cx1, cy1, cx2, cy2]`. They are `an`’s LEGACY family
([`an.timing.easing`](_autosummary/an.timing.easing.html.md#module-an.timing.easing)): `ease` and `ease_in_out` are the quadratic
ease-in-out, not CSS `ease`; the CSS curves are the hyphenated names
(`ease-in-out`, `cubic-bezier(...)`) of the `css` family.

### an.base.EasingSpec *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* *= str | tuple[float, float, float, float] | list[float]*

Either an easing preset name or a 4-tuple cubic-Bézier control [cx1,cy1,cx2,cy2].

### an.base.FILM_AUDIO_SAMPLE_RATE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 44100*

The film mix’s sample rate and channel count. The SAME values the per-shot
audio mux (`an.stage.render._ffmpeg_add_audio`) writes, so a film
assembled from sources and one concatenated from shot mp4s carry the same
audio format.

### an.base.MP4_FASTSTART_ARGS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('-movflags', '+faststart')*

Put the mp4’s `moov` atom in front of `mdat`, so a player can start before
the file has finished downloading.

Here, and not beside one of the ffmpeg calls, because \*\*three separate
commands build the one file a user receives\*\* – the frame mux
(`_ffmpeg_mux`), the audio mux (`_ffmpeg_add_audio`) and the concat
(`_ffmpeg_concat`) – and each of the last two re-lays the container with
`-c copy`, which writes `moov` LAST. The flag was on the first of those
alone, which reads as done and delivers nothing: it applied only to
`silent.mp4`, a per-shot intermediate that is never handed to anyone.
Measured on a local example render (these mp4s are gitignored build
products; `git ls-files` tracks exactly one, and it is moov-last too):
`.an/render_work/shot_s1/silent.mp4` is `ftyp moov free mdat`, while the
per-shot mp4, `artifacts/shots/*.mp4` and `output/main.mp4` are all
`ftyp free mdat moov`. Single-shot and multi-shot alike; an#57’s
“single-shot ones keep it” is wrong, because the `shutil.copy` branch
copies a file that already lost it.

Deliberately NOT part of `DETERMINISTIC_X264_ARGS`. That tuple is an
`ENCODE_ENV_PATHS` comparability key (an/bench/compare.py:98), and this flag
moves no number the panel reads: with `-c copy` it is a container rewrite,
not a re-encode. Verified on ffmpeg 8.1 – elementary-stream sha256
identical, video/audio packet totals identical, file size identical,
decoded YUV sha256 identical, wall time unchanged.

### an.base.PLANE_REST_VALUES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= {'perspective': 1.0, 'plane_fade_end': 0.0, 'plane_fade_start': 0.0, 'rotation_x': 0.0}*

a node
targeted with any of them is drawn on a plane tilted away from the camera
(`rotation_x`, radians, positive = the top recedes; `perspective`, the
eye’s distance in frame heights; `plane_fade_start`/`plane_fade_end`,
the far fade, scene px up the plane from the hinge, off while the end is
`<= 0`). See `runtime.js` “Planes” and `an.stage.timeline.Transform2D`.
Their rest values — the untilted plane, the eye one frame height away, no
far fade — the one table the compiler’s rest table, the Python projection
(`an.stage.timeline`) and `runtime.js`’s `PLANE_PROPERTIES` (pinned by a
test) agree on.

* **Type:**
  The plane properties inside `TRANSFORM_PROPERTIES` (an#314)

### an.base.PathStr

Slash-delimited node path, e.g. `"charlie/head/mouth"`.

The example matters: an unknown target now RAISES rather than being skipped,
and the long-standing `"charlie/torso/left_arm"` illustration names a node
no rig actually builds — the cutout rigs are flat, so arms are siblings of
the torso, not children of it.

### an.base.RendererName

Which renderer draws a shot. The orchestrator uses this to pick an adapter.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘cutout’, ‘stage’, ‘manim’, ‘motion_graphics’, ‘whiteboard’]

### an.base.SCHEMA_VERSION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '0.3.0'*

Current Scene IR schema version. Bump on additive changes; on breaking
changes, also bump COMPATIBLE_VERSION and add a migration in `ir.migrate`.
0.2.0 renamed `Shot.style` -> `Shot.renderer` and `Meta.default_style` ->
`Meta.default_renderer`, and retired `AssetRef(kind="style")` (an#106).
0.3.0 removed `Camera.position` / `.target` / `.focal_length`, which
described a 3D camera this package never had, and gave `Camera` a `keys`
list so it can translate (an#109).

### an.base.SUPPORTED_RENDERERS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('cutout', 'stage', 'manim', 'motion_graphics', 'whiteboard')*

The same vocabulary as [`RendererName`](_autosummary/an.base.html.md#an.base.RendererName), as a runtime tuple — DERIVED
from it, because a hand-typed second copy is a second SSOT that drifts on
the day a renderer is added and nothing fails.

### an.base.SWAP_SET_NAME_FORBIDDEN_SUBSTRINGS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('/', '::')*

`/` would read
as a path segment and `::` is the runtime’s pose-key separator.

* **Type:**
  Characters within which a swap-set name is not addressable

### an.base.Seconds

Time in seconds. Floats at the IR boundary; rational time is used internally
only inside the audio pipeline where drift matters.

### an.base.TRANSITION_KINDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('cut', 'fade', 'dissolve', 'wipe')*

The transition kinds a shot may be entered by. `cut` is the default and
what every document written before transitions existed means.

### an.base.TRIM_PROPERTIES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'dash_offset', 'trim_end', 'trim_start'})*

The path-only properties inside `TRANSFORM_PROPERTIES` (an#160; the
name predates `dash_offset`, an#161): trim and the dash phase. A node that
draws no path refuses all of them.

### an.base.WIPE_DIRECTIONS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('left', 'right', 'up', 'down')*

The ways a `wipe`’s edge can travel across the frame (an#390).

### an.base.swap_set_name_problem(name)

Why `name` cannot be a swap-set name, or `None` if it can.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> swap_set_name_problem("hands") is None
True
>>> swap_set_name_problem("alpha")
"'alpha' is a transform property; the runtime's static switch would shadow the set"
>>> swap_set_name_problem("a::b")
"'a::b' contains '::', which is reserved"
```


# _autosummary/an.bench.capture.html.md

# an.bench.capture

Render one corpus fixture into a throwaway copy, and hand back its artifacts.

Two things this module exists to get right, both of which produce plausible
numbers when got wrong:

**Render into a copy.** The render path mutates the project directory — scene
mtimes, the decisions log, `.an/render_work` — so rendering in place makes
the git sha in the ledger filename a lie about the tree that produced the row.

**Do not inherit a stale render.** `shutil.copytree` of a developer’s
checkout would carry `.an/render_work` and `output/` across, `frames/` is
never cleared, and ffmpeg’s image2 demuxer reads the contiguous
`frame_%06d.png` run from 0 — so a longer previous render is silently
appended to this one. Every encode-side metric pairs source frame *i* with
decoded frame *i*, so that appends garbage to one leg and shifts nothing on the
other. `artifacts/` is deliberately kept *except for one subdirectory*: it
holds the audio cache, whose warm/cold state is recorded rather than destroyed
— but `artifacts/shots` is `mall["shots"]`, the previous render’s per-shot
mp4s, and this module’s whole promise is that nothing of a previous render
crosses. It is gitignored, so it does not reproduce on a clean checkout: a
per-developer landmine, in the module whose docstring says the opposite.

### Functions

| [`capture_fixture`](_autosummary/an.bench.capture.html.md#an.bench.capture.capture_fixture)(name, fixture, \*, repo_root)   | Render `fixture` in a throwaway copy and return its artifacts.             |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`cleanup`](_autosummary/an.bench.capture.html.md#an.bench.capture.cleanup)(capture)                                | Remove a capture's throwaway tree.                                         |
| [`dirty_paths`](_autosummary/an.bench.capture.html.md#an.bench.capture.dirty_paths)(repo_root)                          | `git status --porcelain` lines, so a capture can prove it touched nothing. |
| [`distinct_png_sizes`](_autosummary/an.bench.capture.html.md#an.bench.capture.distinct_png_sizes)(frames_dir)                  | Every distinct `(width, height)` among a shot's frame PNGs, sorted.        |
| [`expected_frame_count`](_autosummary/an.bench.capture.html.md#an.bench.capture.expected_frame_count)(duration, fps)             | The renderer's own frame-count expression, reused rather than restated.    |

### Classes

| [`SceneCapture`](_autosummary/an.bench.capture.html.md#an.bench.capture.SceneCapture)(name, source, prepared, ...[, ...])   | One fixture's whole render.    |
|-----------------------------------------------------------------------------------------------------|--------------------------------|
| [`ShotCapture`](_autosummary/an.bench.capture.html.md#an.bench.capture.ShotCapture)(shot_id, frames_dir, scene_json, ...)  | One rendered shot's artifacts. |

### Exceptions

| [`GitStatusUnavailable`](_autosummary/an.bench.capture.html.md#an.bench.capture.GitStatusUnavailable)   | `git status` did not answer, so "the tree is clean" is not known.   |
|-------------------------------------------------------------------------|---------------------------------------------------------------------|

### *exception* an.bench.capture.GitStatusUnavailable

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

`git status` did not answer, so “the tree is clean” is not known.

Separated from an empty result on purpose. `git status` failing prints
nothing to stdout, so `check=False` turned every failure into “no dirty
paths” — indistinguishable from a clean tree, and *silently* so. Measured:
a concurrent `git` in a linked worktree of this repo takes `index.lock`,
`git status` exits nonzero with empty stdout, and
`test_a_capture_leaves_the_repository_untouched` fails with `[] != [...]`
— an assertion about the capture, pointing at nothing, in a run that has
been green fifty times. A check that could not run is not evidence that
nothing is wrong.

### *class* an.bench.capture.SceneCapture(name, source, prepared, project_dir, mp4, shots, resolution, fps, duration, n_declared_entity_refs, visual_kinds, asset_resolution, audio_cache, wall_seconds, determinism=<factory>, capture='', film=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One fixture’s whole render.

#### capture *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

How the frames left the browser (`"screenshot"` / `"canvas"`),
resolved the way the render resolves it. The decoded pixels are the same
either way, so no metric moves — but `wall_seconds` does, several-fold,
and a timing row is only readable beside the path that produced it
(an#192 flipped the default).

#### film *: [ShotCapture](_autosummary/an.bench.capture.html.md#an.bench.capture.ShotCapture) | [None](https://docs.python.org/3/builtins/constants.html#None)*

An ASSEMBLED scene’s composed frames (transitions, a sound layer —
`an.assemble`), as one segment: what the delivered mp4 shows. `None`
for a scene that is the concatenation of its shots, which is every scene
before an#279’s core corpus. When set, every metric, the golden frames
and the frame count read IT — pairing the shots’ frames against a film
whose dissolves overlap them would measure the overlap, not the encoder.

#### *property* frame_segments *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[ShotCapture](_autosummary/an.bench.capture.html.md#an.bench.capture.ShotCapture)]*

The frame sequence(s) the delivered mp4 shows, in order.

### *class* an.bench.capture.ShotCapture(shot_id, frames_dir, scene_json, runtime_dir, frame_count, duration=0.0, frame_sizes=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One rendered shot’s artifacts.

#### duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The shot’s declared duration, from the IR rather than from the staged
scene, so the expected frame count is derived from the same number the
renderer used.

#### frame_sizes *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int)], ...]*

The distinct pixel sizes actually on disk, from each PNG’s IHDR. The
independent half of a pair whose other half — `SceneCapture.resolution`
— comes from the staged scene’s `meta` and never from a file. Empty
only when the shot wrote no frames.

### an.bench.capture.capture_fixture(name, fixture, , repo_root, keep_render=None)

Render `fixture` in a throwaway copy and return its artifacts.

The copy lives until the caller is done with it — the metrics read the
frames — so this is a context-free function that leaves the tree in place
and hands back the path. `captured()` is the scoped form.

* **Return type:**
  [`SceneCapture`](_autosummary/an.bench.capture.html.md#an.bench.capture.SceneCapture)

### an.bench.capture.cleanup(capture)

Remove a capture’s throwaway tree.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.bench.capture.dirty_paths(repo_root)

`git status --porcelain` lines, so a capture can prove it touched nothing.

Raises [`GitStatusUnavailable`](_autosummary/an.bench.capture.html.md#an.bench.capture.GitStatusUnavailable) when git does not answer, rather than
reporting a clean tree it never observed.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.bench.capture.distinct_png_sizes(frames_dir)

Every distinct `(width, height)` among a shot’s frame PNGs, sorted.

Read from each file’s IHDR — 24 bytes per frame — so reading all of them
costs nothing and catches what sampling one would miss: a sequence whose
size changes partway through, which is what a half-applied supersample
produces.

**Recorded here, enforced elsewhere.** Rendering a fixture at a size the
scene does not declare is a legitimate thing to do —
`misc/bench/wave3_ab.py` patches `runtime.js` to `resolution: k,
autoDensity: false` and drives [`capture_fixture()`](_autosummary/an.bench.capture.html.md#an.bench.capture.capture_fixture) directly to measure
the supersample — so this module reports what it saw and
[`an.bench.run`](_autosummary/an.bench.run.html.md#module-an.bench.run) is where the *bench’s* invariant is asserted.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.bench.capture.expected_frame_count(duration, fps)

The renderer’s own frame-count expression, reused rather than restated.

`max(1, int(round(duration * fps)))` — and Python 3’s `round` is
banker’s rounding, so `math.ceil` or `int(x + 0.5)` silently disagrees
on every half-frame duration.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> expected_frame_count(2.5, 24)
60
>>> expected_frame_count(0.0, 24)
1
```


# _autosummary/an.bench.compare.html.md

# an.bench.compare

`an bench --compare`: read two ledger rows, and **refuse when they are not comparable**.

Refusing is the feature. Two rows measured on different scenes, at different
resolutions, or on different x264 builds are not “one better and one worse” —
every number in them is **uninterpretable** relative to the other, and a number
reported across incomparable rows is worse than no number at all.

Three things here that are easy to get wrong in a way that still produces a
plausible verdict:

**A single per-metric sign mis-reports one of the two mutations.** The sharpness
family moves in *opposite* directions for AA-off and high-CRF, so the direction
is read per metric *and per mutation*, from the `under_mutation` block the row
already carries. Where the optimum is interior (`edge_transition_width` — under
1 is a staircase, 3+ is soft) there is no “better” direction at all, and the
comparison says `changed`, never `regression`. Inventing a band there would
be the silently-widened threshold this whole wave exists to prevent.

**The two sides have opposite comparison rules, and both are measured.**
Render-side rows compare across any machine: zero differing pixels across arm64
macOS, x86-64 Linux and arm64 Linux, across two SwiftShader JIT backends — \*at a
pinned Chromium build\*, which is why the browser build is itself a render-side
comparability key. Encode-side rows are **machine-scoped**: same ISA and x264
build is byte-identical, a different ISA moves the decoded stream a little, and a
different x264 build moves it by two orders of magnitude. A band wide enough to
absorb that would swallow `flat_field_deviation`’s entire crf18->23 signal
(0.0003 -> 0.0005), so the answer is to refuse, not to widen.

**There is no tolerance, and none is needed.** Every comparison is exact.
Measured: two consecutive `an bench` runs on the same machine produce
**bit-identical** numbers for every metric on all six scenes, and identical
`source_pixels_sha256` — the render is pinned and the encode is pinned, so a
delta of exactly zero is the normal case and any nonzero delta is a real change.
An epsilon here would only hide small real movements.

**A direction must survive the metric’s own knobs** (an#140). The two
hard-threshold counters (`flat_field_deviation`, `encode_flicker_on_held_pixels`)
carry their count at every cell of a small grid of their own free parameters,
and a movement whose direction some cells contradict is reported `unstable`
— neither counted toward a family nor `contrary`. Measured: under 4:2:0 ->
4:4:4, `graded_field`’s flat-field count read +84.2% at tol 6 and -81.6% at
tol 8, and the panel had no way to say so. That is not a tolerance band: every
cell is still compared exactly; what changes is whether the answer is a
direction at all.

A fourth, quieter one: **a metric’s own declaration is a comparability key**.
If `family` or `optimum` changed between the two rows, the metric means
something different in each and the comparison is refused for that metric alone
— which is why every row carries its full `metric_declarations` block rather
than referencing the registry that happened to be installed.

### Module Attributes

| [`SUPPORTED_SCHEMA_VERSIONS`](_autosummary/an.bench.compare.html.md#an.bench.compare.SUPPORTED_SCHEMA_VERSIONS)   | Row schema versions this comparer understands.                                                                                                                             |
|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`SCENE_KEYS`](_autosummary/an.bench.compare.html.md#an.bench.compare.SCENE_KEYS)                  | Scene-provenance fields that must match for ANY metric to be comparable.                                                                                                   |
| [`MASK_PARAM_PATHS`](_autosummary/an.bench.compare.html.md#an.bench.compare.MASK_PARAM_PATHS)            | Mask **parameters**, addressed by path into the scene's `masks` block.                                                                                                     |
| [`RENDER_ENV_PATHS`](_autosummary/an.bench.compare.html.md#an.bench.compare.RENDER_ENV_PATHS)            | Row-provenance paths that must match for a **render-side** metric.                                                                                                         |
| [`ENCODE_ENV_PATHS`](_autosummary/an.bench.compare.html.md#an.bench.compare.ENCODE_ENV_PATHS)            | Row-provenance paths that must match for an **encode-side** metric.                                                                                                        |
| [`COMMON_ENV_PATHS`](_autosummary/an.bench.compare.html.md#an.bench.compare.COMMON_ENV_PATHS)            | Row-provenance paths that must match for **either** side.                                                                                                                  |
| [`DECLARATION_KEYS`](_autosummary/an.bench.compare.html.md#an.bench.compare.DECLARATION_KEYS)            | Per-metric declaration fields that must agree, or the metric means something different in each row.                                                                        |
| [`REQUIRED_FAMILIES`](_autosummary/an.bench.compare.html.md#an.bench.compare.REQUIRED_FAMILIES)           | How many distinct causal families must move as declared for a mutation to count as caught.                                                                                 |
| [`CROSS_CHECKED_FIELDS`](_autosummary/an.bench.compare.html.md#an.bench.compare.CROSS_CHECKED_FIELDS)        | Every field a row stores TWICE — inline on the metric and in `metric_declarations` — except `under_mutation`, whose nested shape gets `_prediction_disagreements` instead. |
| [`SCORING_FIELDS`](_autosummary/an.bench.compare.html.md#an.bench.compare.SCORING_FIELDS)              | The fields of a per-mutation prediction that the verdict actually reads.                                                                                                   |

### Functions

| [`compare`](_autosummary/an.bench.compare.html.md#an.bench.compare.compare)(before, after, \*[, mutation])      | Compare two ledger rows.                                               |
|----------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`direction_of`](_autosummary/an.bench.compare.html.md#an.bench.compare.direction_of)(before, after)                 | `increase` / `decrease` / `no_change` — exactly, with no tolerance.    |
| [`format_comparison`](_autosummary/an.bench.compare.html.md#an.bench.compare.format_comparison)(report)                   | The human-readable digest.                                             |
| [`latest_rows`](_autosummary/an.bench.compare.html.md#an.bench.compare.latest_rows)(\*[, root, count])              |                                                                        |
| [`load_row`](_autosummary/an.bench.compare.html.md#an.bench.compare.load_row)(path)                              | Read one ledger row from disk.                                         |
| [`readable_sweep`](_autosummary/an.bench.compare.html.md#an.bench.compare.readable_sweep)(sweep)                       | `sweep` if every cell is a possible `[counted, of]` pair, else `None`. |
| [`sweep_verdict`](_autosummary/an.bench.compare.html.md#an.bench.compare.sweep_verdict)(before, after, \*[, shipped]) | Does a threshold counter's direction survive its own parameter grid?   |

### Exceptions

| [`ComparisonError`](_autosummary/an.bench.compare.html.md#an.bench.compare.ComparisonError)   | The comparer was handed something it cannot read at all.   |
|--------------------------------------------------------------------|------------------------------------------------------------|

### an.bench.compare.COMMON_ENV_PATHS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...], ...]* *= (('render_kwargs',),)*

Row-provenance paths that must match for **either** side.

### an.bench.compare.CROSS_CHECKED_FIELDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('family', 'side', 'comparison_scope', 'reference')*

Every field a row stores TWICE — inline on the metric and in
`metric_declarations` — except `under_mutation`, whose nested shape gets
`_prediction_disagreements` instead. Both copies are written from one
registry at one moment, so a disagreement means the row was edited, and
`compare` reads the INLINE one.

Three defects of this exact class were found in one review pass, each on a
field that had been left out: `family` (moved a witness between families and
took `criterion_met_on` from three scenes to five), `under_mutation` (flipped
`contrary` to `as_declared`), and `comparison_scope` (compared an encode-side
metric across a different ISA). The list is therefore checked for
COMPLETENESS by a test rather than maintained by hand — see
`tests/test_bench_compare.py::test_every_doubly_stored_field_is_cross_checked`.

### *exception* an.bench.compare.ComparisonError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

The comparer was handed something it cannot read at all.

### an.bench.compare.DECLARATION_KEYS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('family', 'side', 'optimum', 'unit')*

Per-metric declaration fields that must agree, or the metric means something
different in each row. `optimum` decides which way “better” points and
`family` decides what an#41’s criterion counts.

### an.bench.compare.ENCODE_ENV_PATHS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...], ...]* *= (('environment', 'encode_side', 'isa'), ('environment', 'encode_side', 'x264_sei'), ('environment', 'encode_side', 'x264_argv'), ('environment', 'encode_side', 'pix_fmt'), ('environment', 'encode_side', 'scale_filter'), ('encode_command_source',), ('decode_commands',))*

Row-provenance paths that must match for an **encode-side** metric.

### an.bench.compare.MASK_PARAM_PATHS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...], ...]* *= (('masks', 'edge', 'operator'), ('masks', 'edge', 'threshold'), ('masks', 'flat', 'operator'), ('masks', 'flat', 'dilate_k'), ('masks', 'held', 'operator'), ('masks', 'ring', 'operator'), ('masks', 'render_edge', 'operator'), ('masks', 'render_edge', 'threshold'))*

Mask **parameters**, addressed by path into the scene’s `masks` block. The
counts and fractions beside them are measurements and are excluded.

### an.bench.compare.RENDER_ENV_PATHS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...], ...]* *= (('environment', 'render_side', 'chromium_build'), ('environment', 'render_side', 'playwright'), ('environment', 'render_side', 'launch_argv'))*

Row-provenance paths that must match for a **render-side** metric.
The Chromium build is here and not merely informational: the cross-arch
verdict measured ISA- and OS-invariance *at a pinned build*, and an#38’s
golden path keys on the build for the same reason. One bump has been measured
to move zero pixels (1187 -> 1223), so this refusal is precautionary rather
than a known break — and a deliberate re-bless is the intended response.

### an.bench.compare.REQUIRED_FAMILIES *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 3*

How many distinct causal families must move as declared for a mutation to
count as caught. an#41’s criterion, restated from the research: “>=3 metrics
from >=3 distinct causal families, evaluated per mutation, with a per-metric
per-mutation sign declared in advance”.

### an.bench.compare.SCENE_KEYS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('scene_contract_sha256', 'resolution', 'fps', 'n_frames', 'shot_order', 'palette_hex', 'tolerances')*

Scene-provenance fields that must match for ANY metric to be comparable.
Deliberately not the whole provenance block: that also carries per-run
*measurements* (mask pixel counts, `wall_seconds`, the golden diagnostics),
which change exactly when the render changes — i.e. when the two rows are
most worth comparing.

### an.bench.compare.SCORING_FIELDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('expect', 'counts', 'gate', 'state')*

The fields of a per-mutation prediction that the verdict actually reads.
`reason` is prose and is deliberately NOT here — the declarations block
carries it and the inline block drops it, so requiring it would refuse every
real row.

### an.bench.compare.SUPPORTED_SCHEMA_VERSIONS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), ...]* *= (1,)*

Row schema versions this comparer understands. A row it cannot read is
refused rather than guessed at — the whole point of the version field.

### an.bench.compare.compare(before, after, , mutation=None)

Compare two ledger rows. Returns a report; raises only on an unreadable row.

`mutation` selects the question. With one, the report answers an#41’s:
did the declared witnesses move in the declared direction, and did at least
three distinct causal families do so. Without one, it answers “is the second
row worse”, which only the one-sided metrics can answer at all.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.compare.direction_of(before, after)

`increase` / `decrease` / `no_change` — exactly, with no tolerance.

Two consecutive runs on one machine are bit-identical, so zero is the normal
delta and any nonzero one is real. Booleans compare as booleans: a tripwire
that went `True -> False` has not “decreased”.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> direction_of(1.0, 1.0)
'no_change'
>>> direction_of(True, False)
'decrease'
```

### an.bench.compare.format_comparison(report)

The human-readable digest. Refusals first, because they are the verdict.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.bench.compare.latest_rows(, root=None, count=2)

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

The most recent committed ledger rows, newest last — \*\*by 

```
`
```

generated_at\`\*\*.

Not by filename. Filenames are `<date>-<sha7>[-dirty].json`, so a filename
sort orders same-day rows by *sha hex*. On a Wave 3 PR the re-baseline and
the after-run are plausibly the same day, and when the after-commit’s sha
sorts lower a bare `an bench-compare` silently swaps before and after and
reports every improvement as a regression.

A row whose `generated_at` cannot be read sorts **before** every dated
one, so it is dropped as soon as two dated rows exist: a corrupt or
pre-schema file must never become the `after` row a verdict is drawn from.
The filename stays the tiebreak, which is what keeps a directory whose rows
all lack the key ordered by date.

`-dirty` rows are excluded: a row measured against uncommitted edits
describes no commit, and comparing one is comparing against nothing
nameable. `an bench --out` still lets a caller point at one explicitly.

### an.bench.compare.load_row(path)

Read one ledger row from disk.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.compare.readable_sweep(sweep)

`sweep` if every cell is a possible `[counted, of]` pair, else `None`.

Possible means integers (a bool is not a count), `of > 0` and
`0 <= counted <= of` — a cell no measurement could have produced is an
edited row, and reading it would let the edit choose the verdict.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> readable_sweep({"t=1": [3, 10]})
{'t=1': [3, 10]}
>>> readable_sweep({"t=1": [11, 10]}) is None
True
>>> readable_sweep({"t=1": [True, 10]}) is None
True
```

### an.bench.compare.sweep_verdict(before, after, , shipped=None)

Does a threshold counter’s direction survive its own parameter grid?

an#140. `flat_field_deviation` counts pixels past a hard threshold over a
mask eroded by a second free parameter, and a row carries its count at
every cell of a declared grid of both (`sweep`: `{cell: [counted, of]}`).
This reads the direction at every cell the two rows SHARE. When some cells
rise and others fall, the shipped value’s direction is a property of where
the threshold happened to be set, not of the change under test — measured,
a 4:2:0 -> 4:4:4 change moved `graded_field` +84.2% at tol 6 and -81.6%
at tol 8 — and `state` is `unstable`. `no_change` cells contradict
nothing.

Exact, like everything else here: fractions are compared by cross-multiplying
the integer counts, never as rounded floats.

Shared cells rather than identical grids: a dilation whose mask is empty in
one row is omitted from that row, and discarding the other fourteen cells
for it would let the verdict count unchecked. `unknown` only when a side
has no readable sweep, or the `shipped` cell is not among the shared ones
— the check could not run, which is a caveat and not a refusal, because a
row written before an#140 has no sweep and is otherwise perfectly readable.
(A row that DECLARES a sweep and carries none is refused upstream, in
`_compare_scene`.)

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> sweep_verdict({"t=1": [1, 10], "t=2": [5, 10]}, {"t=1": [2, 10], "t=2": [4, 10]})["state"]
'unstable'
>>> sweep_verdict({"t=1": [1, 10]}, {"t=1": [2, 20]})["state"]
'stable'
>>> sweep_verdict({"t=1": [1, 10]}, None)["state"]
'unknown'
>>> sweep_verdict(None, None) is None
True
```


# _autosummary/an.bench.contract.html.md

# an.bench.contract

`scene_contract_sha256`: the fact that decides whether two rows are comparable.

Two ledger rows measured on different scenes are not “one better and one
worse” — every metric in them is **uninterpretable** relative to the other.
`edge_transition_width`’s own docstring says so: the absolute value is
scene-dependent, because a deliberately 3px black outline is legitimately
non-flat.

So each row carries a hash of the thing that was actually rendered. Deliberately
a hash of the **staged compiled scene**, not of the project directory: the
directory carries scene mtimes and a decisions log that move on every load, and
hashing it would make every row incomparable with every other for reasons that
never reached a pixel.

### Functions

| [`count_drawable_entities`](_autosummary/an.bench.contract.html.md#an.bench.contract.count_drawable_entities)(scene_json)   | Top-level entities under the synthetic root.                                |
|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`count_nodes`](_autosummary/an.bench.contract.html.md#an.bench.contract.count_nodes)(scene_json)               | Every node in the tree, root included.                                      |
| [`frames_sha256`](_autosummary/an.bench.contract.html.md#an.bench.contract.frames_sha256)(frame_paths)            | `sha256` over the DECODED pixels of a frame sequence, never file bytes.     |
| [`scene_contract_sha256`](_autosummary/an.bench.contract.html.md#an.bench.contract.scene_contract_sha256)(scene_json)     | A stable digest of what was rendered.                                       |
| [`scenes_contract_sha256`](_autosummary/an.bench.contract.html.md#an.bench.contract.scenes_contract_sha256)(scene_jsons)   | The contract digest for a whole scene, across every shot in timeline order. |

### an.bench.contract.count_drawable_entities(scene_json)

Top-level entities under the synthetic root.

Named for what it counts. “n_entities” is ambiguous in this codebase — the
IR’s `shot.entities` includes `voice` refs that configure
the render rather than appearing in it, so the two numbers differ on every
scene with dialogue. Both are recorded; only this one is inside the hash.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> count_drawable_entities({"scene": {"children": [{}, {}]}})
2
```

### an.bench.contract.count_nodes(scene_json)

Every node in the tree, root included.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> count_nodes({"scene": {"children": [{"children": [{}]}]}})
3
```

### an.bench.contract.frames_sha256(frame_paths)

`sha256` over the DECODED pixels of a frame sequence, never file bytes.

The gate for `encode_flicker_on_held_pixels`: without it, half-res-then-
nearest-upscale — the most visible possible flat-art regression — reports a
7.1x *improvement*, because a flattened render gives x264 large uniform
skip regions.

Decoded, not file bytes, for the reason the cross-architecture verdict
records: Chromium 1187 -> 1223 changes 144/144 PNG files and **zero**
pixels, so a file-byte digest goes red on the first Playwright bump for a
reason unrelated to animation quality.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.bench.contract.scene_contract_sha256(scene_json)

A stable digest of what was rendered.

Hashed over a *reduced* form rather than the whole document, and the
reduction is the point: the digest must move when the picture’s contract
moves and stay put otherwise. Animation keyframe floats and asset paths are
in; nothing time- or machine-dependent is, because the compiled scene is
already free of both (every escaping set goes through `sorted()`, the
palette hash is `sum(ord(c)) % 5` rather than Python’s `hash()`, and
the staged JSON is written with `sort_keys=True`).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> a = scene_contract_sha256({"meta": {"fps": 24}, "scene": {"children": []}})
>>> a == scene_contract_sha256({"scene": {"children": []}, "meta": {"fps": 24}})
True
>>> len(a)
64
```

### an.bench.contract.scenes_contract_sha256(scene_jsons)

The contract digest for a whole scene, across every shot in timeline order.

A single-shot scene returns **exactly** [`scene_contract_sha256()`](_autosummary/an.bench.contract.html.md#an.bench.contract.scene_contract_sha256) of its
one staged scene, so every row written before the corpus grew a multi-shot
fixture stays comparable — `an bench --compare` refuses rows whose
contract hash differs, and a gratuitous change here would retire the only
committed row as evidence.

A multi-shot scene hashes the ordered list of per-shot digests, because
hashing only the first shot would let a change to the second one pass as
“the same scene” — which is precisely the claim this digest exists to deny.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> a = {"scene": {"children": []}}
>>> scenes_contract_sha256([a]) == scene_contract_sha256(a)
True
>>> scenes_contract_sha256([a, a]) == scene_contract_sha256(a)
False
```


# _autosummary/an.bench.core_corpus.html.md

# an.bench.core_corpus

The core corpus: what `an` renders with no character and no genre (ADR 0001 decision 7).

Before any module leaves `an` for `cutan` (P8), the core keeps a pixel gate
of its own: **paths, text, planes, camera and transitions**, rendered by the
stage engine (`an.stage`) with no character, with goldens and contract hashes
of their own. These scenes are [`CORE_FIXTURES`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.CORE_FIXTURES):

They live beside the cut-out corpus in `misc/bench/corpus/` and run in the
same `an bench` (the cut-out corpus imports them, in
[`an.bench.corpus`](_autosummary/an.bench.corpus.html.md#module-an.bench.corpus)), so they are blessed by the same protocol and their
contract hashes are checked by the same default-leg guard. What makes them the
CORE corpus is what they need: `tests/test_core_corpus.py` renders every one
of them with NO GENRE REGISTERED and checks each pinned frame against its bless
record. That they render with no cut-out CODE is proven too, since the move
(an#225): the same tests run with every cut-out module poisoned. The cut-out
corpus lives in `cutan` (`cutan.bench`); this module, the scenes and their
goldens stay.

This module is CORE (behind no firewall): the fixture type, the pinned render
knobs, the throwaway copy and the browser-free contract hash moved here from
[`an.bench.corpus`](_autosummary/an.bench.corpus.html.md#module-an.bench.corpus) and [`an.bench.capture`](_autosummary/an.bench.capture.html.md#module-an.bench.capture) (genre), which re-export
them. Nothing here imports the stage at module level.

```pycon
>>> sorted(CORE_FIXTURES)
['after_plane', 'front_plane', 'path_draw', 'path_shape', 'path_taper', 'path_wobble', 'rig_chain', 'rig_interleave', 'rig_order', 'rig_origin', 'rig_rest', 'stage_pan', 'text_card', 'text_counter', 'text_outline', 'text_swap', 'transitions']
```

### Module Attributes

| [`BENCH_RENDER_KWARGS`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.BENCH_RENDER_KWARGS)      | Rendering knobs pinned for every bench capture, recorded verbatim into the ledger.                                                                                                                                                           |
|---------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`CORPUS_DIRNAME`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.CORPUS_DIRNAME)           | Where the bench-owned fixtures live.                                                                                                                                                                                                         |
| [`CORE_FIXTURES`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.CORE_FIXTURES)            | The core corpus (see the module docstring).                                                                                                                                                                                                  |
| [`IGNORED_ON_COPY`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.IGNORED_ON_COPY)          | they are the previous render's output, and one of them silently extends this one's frame sequence.                                                                                                                                           |
| [`IGNORED_RELPATHS_ON_COPY`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.IGNORED_RELPATHS_ON_COPY) | Excluded by their path **relative to the project root**, POSIX-spelled.                                                                                                                                                                      |
| [`RENDER_WORK_RELPATH`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.RENDER_WORK_RELPATH)      | Where the renderer leaves its per-shot working tree inside the project.                                                                                                                                                                      |
| [`FRAME_PNG_GLOB`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.FRAME_PNG_GLOB)           | How a shot's frames are named on disk.                                                                                                                                                                                                       |
| [`FILM_SEGMENT_ID`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.FILM_SEGMENT_ID)          | The id the bench gives an ASSEMBLED film's frames (transitions, a sound layer) when it measures them as one segment — what the delivered mp4 shows.                                                                                          |
| [`FILM_FRAMES_RELPATH`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.FILM_FRAMES_RELPATH)      | Where an assembled scene's composed frames are written, under the render's work directory, by [`compose_film_frames()`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.compose_film_frames) (the film itself is a concat of segments and never holds them, an#260). |

### Functions

| [`compiled_contract_sha256`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.compiled_contract_sha256)(fixture, \*, repo_root)   | The `scene_contract_sha256` a render of `fixture` would record — no browser.                                                                                                                                                                                           |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`compose_film_frames`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.compose_film_frames)(scene, work_dir)               | An assembled scene's composed frames, written under `work_dir` from its shots' frames ([`an.assemble.write_film_frames()`](_autosummary/an.assemble.html.md#an.assemble.write_film_frames)); `None` for a scene that is a plain concatenation of its shots. |
| [`frames_dir_for`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.frames_dir_for)(work_dir, shot_id)                  | Where a render left the frames of `shot_id` (or of the composed film).                                                                                                                                                                                                 |
| [`golden_agreement`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.golden_agreement)(name, work_dir, \*, ...[, root])  | `{frame key: (blessed sha256, today's sha256)}` for every frame the committed bless record of `name` pins, read from a render's work dir.                                                                                                                              |
| [`render_fixture`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.render_fixture)(fixture, \*, repo_root, base)       | Render `fixture` cold, the way the bench does, in a copy under `base`.                                                                                                                                                                                                 |
| [`stage_copy`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.stage_copy)(fixture_dir, base)                      | Copy a fixture into `base`, leaving the previous render behind.                                                                                                                                                                                                        |

### Classes

| [`Fixture`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.Fixture)(path[, prepare, ...])   | A corpus scene: where it lives, how to build it, what it must render.   |
|----------------------------------------------------------------------------------|-------------------------------------------------------------------------|

### Exceptions

| [`CaptureError`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.CaptureError)   | A capture could not produce something the metrics need.   |
|-----------------------------------------------------------------|-----------------------------------------------------------|

### an.bench.core_corpus.BENCH_RENDER_KWARGS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]* *= {'auto_audio': False, 'parallel': 1, 'strict_assets': True}*

Rendering knobs pinned for every bench capture, recorded verbatim into the
ledger. NOT flags: a bench whose render knobs vary per invocation produces
incomparable rows.

`auto_audio=False` because audio cannot move a pixel and would otherwise
make the frames depend on the audio cache’s warm/cold state; `parallel=1`
because a timing-sensitive pool is one more thing to explain if the pixels
ever do differ; `strict_assets=True` because a stand-in asset renders
happily as a DIFFERENT picture (an#33).

### an.bench.core_corpus.CORE_FIXTURES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Fixture](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.Fixture)]* *= {'after_plane': Fixture(path='misc/bench/corpus/after_plane', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="a prop placed BETWEEN two planes under a pan (an#344): a far sky (depth 0.25), a disc with \`stage.after: set/sky\`, then a wall in two pieces with a window gap and a sill, all at depth 1.0, while the camera pans 60 px. The disc is drawn behind the walls and rides the sky's parallax through its band wrapper: between the goldens the walls move 40 px and the sky and disc 10 px. A regression in the draw order (the disc over the wall), in the wrapper's compensation (the disc sliding against the sky), or in the band containers' addressing moves a golden."), 'front_plane': Fixture(path='misc/bench/corpus/front_plane', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="an environment cut by \`characters_after\` with an ANIMATED foreground plane (an#343): \`stage\` draws \`sky\` and \`hill\` behind a world text label and \`rail\` in front of it, in its own container with \`scope: stage\`, so the rail is addressed \`stage/rail\` (never \`stage_\_front/rail\`) and its \`y\` tween rises from 60 to 0 over the label. What moves between the goldens is the rail crossing the label: a regression in the runtime's scoped indexing (the channel would name nothing and the load throws), in the cut, or in the draw order moves a golden."), 'path_draw': Fixture(path='misc/bench/corpus/path_draw', prepare=None, expect_visual_kinds=frozenset({'path'}), golden_frames=(0.0, 0.3333333333333333), golden_note="two stroked paths (an#160, an#161), both dashed and both coloured by a StylePack's \`stroke\` role: a marching-ants frame whose \`dash_offset\` runs 0 -> 20 px, and a cubic arrow that draws itself on (\`trim_end\` 0 -> 1) with its head on the moving tip. What moves between the goldens is the ROUTE growing (frame 0 shows none of it) and the frame's dashes sliding 6.7 px along their path; a regression in trim, in the dash phase, in the anchored-at-the-path-start rule that keeps a dash from crawling as the tip advances, or in the pack reaching a path, moves a golden. Butt caps, so a dash's ends are exact rather than rounded past their length."), 'path_shape': Fixture(path='misc/bench/corpus/path_shape', prepare=None, expect_visual_kinds=frozenset({'path'}), golden_frames=(0.0, 0.3333333333333333), golden_note='closed and filled paths (an#161): a badge drawn whole (fill under a mitred border joined at its first corner, no caps there), and a region as two entities, a border-less fill whose node \`alpha\` fades 0 -> 1 under a closed border that draws itself on (\`trim_end\` 0 -> 1). Frame 0 shows only the badge; a regression in the fill, the closing join, the fill staying untrimmed or a \`width: 0\` path drawing a stroke moves a golden.'), 'path_taper': Fixture(path='misc/bench/corpus/path_taper', prepare=None, expect_visual_kinds=frozenset({'path'}), golden_frames=(0.0, 0.3333333333333333), golden_note="variable-width paths (an#161), drawn as filled outlines: a wobbled brush stroke swelling from nothing to 1.2x and back to a point, and an arrow widening 0.2x -> 1x along a mitred polyline that draws itself on with its head on the tip. The width is anchored to the WHOLE path, so the arrow's base does not thin as it grows; a regression in the profile, the miter, the anchoring or the fill-instead-of-stroke rule moves a golden."), 'path_wobble': Fixture(path='misc/bench/corpus/path_wobble', prepare=None, expect_visual_kinds=frozenset({'path'}), golden_frames=(0.0, 0.3333333333333333), golden_note='two hand-drawn paths (an#161): a solid frame wobbling 3 px at a 60 px wavelength, and an arrow wobbling 4 px that draws itself on (\`trim_end\` 0 -> 1). The wobble is compile-time (seeded by the entity id, no trigonometry), so the frame is the same in both goldens and only the route grows; a change to the noise, the seed, the end envelope or the resampling moves both.'), 'rig_chain': Fixture(path='misc/bench/corpus/rig_chain', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="a desk-lamp prop with \`nesting: bones\` (an#340): base -> upper arm -> forearm, and a shade NESTED on the forearm's bone (the sword in the hand). The upper arm (rest 30 deg) tweens 0.52 -> -0.3 rad, the forearm (rest -70 deg, relative to the upper arm) -1.22 -> -0.4, the shade 0 -> 0.6: each turns about its joint and carries what hangs from it. A regression that flattened the chain (forearm left behind by the upper arm), inherited a parent's attachment offset, or applied a rest pose twice moves a golden."), 'rig_interleave': Fixture(path='misc/bench/corpus/rig_interleave', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="the rig_chain desk lamp (\`nesting: bones\`, base -> upper arm -> forearm -> shade) with an UNRELATED panel ordered between the upper arm (1) and the forearm (3) (an#430): the panel covers the upper arm's top and the forearm crosses OVER the panel, which no sort of one container's items can give. Painted from the entity's global \`paint_order\`. The same three tweens as rig_chain move the arm between the goldens. A regression that painted the chain together (the panel over both arms, or under both) moves a golden."), 'rig_order': Fixture(path='misc/bench/corpus/rig_order', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="a nested chain drawn BEHIND the part it hangs from (an#403): \`rig_chain\`'s desk lamp in \`nesting: bones\` with the whole arm (upper, forearm, shade: draw orders 1-3) behind its base (4). The base's container sorts its own drawing after the arm (\`sortableChildren\`, \`z_index\`), which the stage used to refuse. Between the goldens the arm swings as in \`rig_chain\`; where it passes over the base the base is in front. A regression in the runtime's sort, the compiler's \`z_index\` or the chain refusal moves a golden."), 'rig_origin': Fixture(path='misc/bench/corpus/rig_origin', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="two copies of one three-bone signpost rig (base, post, sign) at the same \`stage.at\` height (an#338): \`footed\` declares its \`origin\` at the foot of its base, so its foot stands ON the placement line; \`centred\` declares none, so the middle of its bones' extent lands there and it hangs lower. \`footed\` tweens \`rotation\` 0 -> -0.4 rad, which turns it about the declared origin: between the goldens its sign swings left while its foot does not move, and \`centred\` does not move at all. A regression that ignored \`origin\` (both posts at one height), placed parts about the wrong point, or broke the shared rig builder moves a golden."), 'rig_rest': Fixture(path='misc/bench/corpus/rig_rest', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="a tripod prop whose three legs are ONE drawing on three bones with \`rotation_deg\` 22 / 0 / -22 (an#339): the splay is the bones' rest pose, not pixels. The whole tripod tweens \`rotation\` 0 -> 0.3 rad about its declared origin (the centre foot), and the splayed legs ride it: at frame 8 the rig is tilted 0.2 rad with the splay intact. A regression that dropped the rest pose (three parallel legs), applied it twice, or let the entity's rotation replace a leg's instead of composing with it moves a golden."), 'stage_pan': Fixture(path='misc/bench/corpus/stage_pan', prepare=None, expect_visual_kinds=frozenset({'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="three coloured blocks at depths 0.25 / 1.0 / 2.0 under a zoom-free pan (an#111). What moves between the goldens is the SEPARATION: the blocks start aligned and end 10 / 40 / 80 px apart, which is the parallax and nothing else. Frame 8, not the mid-frame: the camera travels 5 px per frame and the far plane moves a quarter of that, so only every fourth frame lands every block on an exact pixel boundary — at any other frame the anti-aliased edge changes the exact-colour mask's SIZE, and a centroid measured against a different shape is not a displacement (the measurement refuses it outright). Zoom is held constant on purpose: the x = 0 probe that cancels it in the JSON half does not reach a centroid, which sits at the plane's own offset."), 'text_card': Fixture(path='misc/bench/corpus/text_card', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="text under a camera push-in and roll (an#279, the core corpus): two OVERLAY words fading in one after the other (\`word_1\` starts 0.125 s after \`word_0\`) and a WORLD label on a plane block, while the camera zooms 1.0 -> 1.3 and rolls 0.12 rad. What moves between the goldens: the words' alpha (frame 0 shows neither), the label and block growing and turning with the camera, and the overlay title NOT turning — a regression that put overlay text in the world, broke per-word addressing or the camera's zoom/roll moves a golden. The face is Pillow's embedded Aileron, so the glyphs do not depend on the machine's fonts."), 'text_counter': Fixture(path='misc/bench/corpus/text_counter', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.5), golden_note="a calendar counting 1 to 30 with ONE text block (an#342): a right-aligned \`counter: {format: '{d}', start: 1}\` under one linear \`tween day value -> 30\` over 1 s, lowered at compile to a replacement set of the strings the frames show. Frame 0 shows 1; frame 12's value is exactly 15.5, which nearest-half-even rounding shows as 16 (15 would mean ties away from even, 15/17 a sampling or set-time shift). A regression in the sampling grid, the half-frame set time, the rounding or the right-edge geometry moves a golden."), 'text_outline': Fixture(path='misc/bench/corpus/text_outline', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="OUTLINED text (an#313, OverSimplified's labels): white words with a 4 px near-black outline over a mid-blue plate and a pale stripe, the second word fading in from 0.125 s. Frame 0 shows the first word alone, outline and all, and NO outline where the second word will be (the outline lives in the word's own texture, so its alpha hides both); frame 8 shows both, legible over the blue and the stripe alike. A regression in the outline's width, its order under the fill, the box growth that keeps it unclipped, or per-word alpha moves a golden."), 'text_swap': Fixture(path='misc/bench/corpus/text_swap', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note='a text block\\'s CONTENT changing within one shot (an#341): one right-aligned \`unit: block\` label whose \`texts\` set is swapped twice, "Day 1" -> "Day 12" at 0.125 s (the entity-level \`set day text d12\`) -> "Day 300" at 0.25 s (the \`day/block_0\` path). What moves between the goldens is the string, growing LEFTWARDS from a fixed right edge: a regression in the swap set, in the per-key geometry anchored on the \`align\` edge, or in the entity-level fan-out moves a golden.'), 'transitions': Fixture(path='misc/bench/corpus/transitions', prepare=None, expect_visual_kinds=frozenset({'path', 'rect'}), golden_frames=(0.08333333333333333, 0.375, 0.625), golden_note="the delivered film's COMPOSED frames (an#279, the core corpus): \`dusk\` fades in from black over 0.25 s, then \`dawn\` dissolves in over 0.25 s (frames 6-11 are the blend; the film is 12 + 12 - 6 = 18 frames). Frame 2 is mid-fade, frame 9 mid-dissolve (both pictures at once), frame 15 \`dawn\` alone with its arrow. A regression in the fade colour, the dissolve weights, the overlap arithmetic or the order of the shots moves a golden. The only fixture measured on the film's frames rather than the shots' — what the delivered mp4 shows (an.bench.capture's film segment).")}*

The core corpus (see the module docstring).

### an.bench.core_corpus.CORPUS_DIRNAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'misc/bench/corpus'*

Where the bench-owned fixtures live. NOT under `examples/`, and the reason
is mechanical rather than tidiness: `.gitignore` excludes every
`examples/*/assets/`, so a corpus scene that needs committed art cannot live
there without a carve-out per scene. `misc/` is not ignored at all.

The second reason is that a metrics fixture must **hold still**. These four
carry their whole rig as committed files and have no `prepare` step, so
their pixels are a function of the repo alone — where `promote_demo`’s are a
function of `cutan.characters.promote`, and would need re-blessing whenever that
changes.

### *exception* an.bench.core_corpus.CaptureError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A capture could not produce something the metrics need.

### an.bench.core_corpus.FILM_FRAMES_RELPATH *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'film_frames'*

Where an assembled scene’s composed frames are written, under the render’s
work directory, by [`compose_film_frames()`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.compose_film_frames) (the film itself is a concat
of segments and never holds them, an#260).

### an.bench.core_corpus.FILM_SEGMENT_ID *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'film'*

The id the bench gives an ASSEMBLED film’s frames (transitions, a sound
layer) when it measures them as one segment — what the delivered mp4 shows.

### an.bench.core_corpus.FRAME_PNG_GLOB *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'frame_\*.png'*

How a shot’s frames are named on disk. One constant rather than the literal
repeated at each glob site.

### *class* an.bench.core_corpus.Fixture(path, prepare=None, expect_visual_kinds=frozenset({}), golden_frames=<factory>, golden_note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A corpus scene: where it lives, how to build it, what it must render.

#### expect_visual_kinds *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)*

Visual kinds the staged scene MUST contain — see the module docstring.

#### golden_frames *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), ...]*

Times (seconds, into the scene’s CONCATENATED timeline) at which a
golden frame is blessed. Two per scene, the second chosen so something
has actually moved — `--bless` refuses a pair whose two frames are
pixel-identical, which is not hypothetical: `promote_demo`’s frame 0 and
its `duration/2` frame differ by exactly **zero** pixels.

#### golden_note *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

One line saying what moves between the two golden times. Carried as data
because “pick a time where something moved” is a rule that decays into a
habit, and the reason is what a reviewer needs when a golden goes red.

#### prepare *: [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path)], [None](https://docs.python.org/3/builtins/constants.html#None)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Run against the throwaway copy before loading, to regenerate build
products the repo does not track.

### an.bench.core_corpus.IGNORED_ON_COPY *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('.an', 'output', '.anima')*

they are the previous render’s
output, and one of them silently extends this one’s frame sequence.
Matched on the **basename**, at any depth — that is exactly what
`shutil.ignore_patterns` does, and it is why `artifacts/shots` cannot be
spelled here. See [`IGNORED_RELPATHS_ON_COPY`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.IGNORED_RELPATHS_ON_COPY).

* **Type:**
  Copied for the render, but never these

### an.bench.core_corpus.IGNORED_RELPATHS_ON_COPY *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('artifacts/shots',)*

Excluded by their path **relative to the project root**, POSIX-spelled.
`mall["shots"]` is `<project>/artifacts/shots`, and `artifacts/`
itself is kept on purpose — it holds the audio cache, whose warm/cold state
this module records rather than destroys.

Neither spelling belongs in [`IGNORED_ON_COPY`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.IGNORED_ON_COPY), and \*\*both fail
silently\*\*. `shutil.ignore_patterns` returns a closure handed the NAMES
inside one directory, which it `fnmatch.filter``s — so ``"artifacts/shots"`
can never match anything (no name contains a separator) and a bare
`"shots"` would delete every directory of that name **anywhere** in the
tree, a character rig’s included.

### an.bench.core_corpus.RENDER_WORK_RELPATH *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '.an/render_work'*

Where the renderer leaves its per-shot working tree inside the project.

### an.bench.core_corpus.compiled_contract_sha256(fixture, , repo_root)

The `scene_contract_sha256` a render of `fixture` would record — no browser.

Compiles every timeline shot the way the cutout renderer does (the scene’s
size, fps, style pack, default easing and stepped-timing policy, with
`strict_assets` as the bench sets it) in a throwaway copy, and hashes the
documents. It is the default-leg twin of `capture_fixture()`: the
contract hash is a function of the compiled JSON alone, so the guards that
check it — against the newest ledger row and against each golden’s bless
record — run on every PR, not only in the labelled browser lane.

It is the contract of a bench render, which passes no overrides: a render
given its own `step_hz`, fps or resolution compiles something else.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.bench.core_corpus.compose_film_frames(scene, work_dir)

An assembled scene’s composed frames, written under `work_dir` from its
shots’ frames ([`an.assemble.write_film_frames()`](_autosummary/an.assemble.html.md#an.assemble.write_film_frames)); `None` for a scene
that is a plain concatenation of its shots.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.bench.core_corpus.frames_dir_for(work_dir, shot_id)

Where a render left the frames of `shot_id` (or of the composed film).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> frames_dir_for(Path("w"), "film").as_posix(), frames_dir_for(Path("w"), "a").as_posix()
('w/film_frames', 'w/shot_a/frames')
```

### an.bench.core_corpus.golden_agreement(name, work_dir, , chromium_build, root=None)

`{frame key: (blessed sha256, today's sha256)}` for every frame the
committed bless record of `name` pins, read from a render’s work dir.

Decoded pixels, never file bytes (`an.bench.golden`’s criterion).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### an.bench.core_corpus.render_fixture(fixture, , repo_root, base)

Render `fixture` cold, the way the bench does, in a copy under `base`.

Through the core API only (`an.project.load` + `an.render.render`) —
no bench capture code — so it runs with the cut-out genre absent. Returns
the render’s work directory.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.bench.core_corpus.stage_copy(fixture_dir, base)

Copy a fixture into `base`, leaving the previous render behind.

Split out of `capture_fixture()` so the exclusion is testable without
rendering anything — which matters, because the failure it prevents is
silent. `frames/` is never cleared and ffmpeg’s image2 demuxer reads the
contiguous `frame_%06d.png` run from 0, so a longer previous render is
appended to this one’s source leg and to nothing else.

Two kinds of exclusion, because one kind cannot say both things:
[`IGNORED_ON_COPY`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.IGNORED_ON_COPY) by basename at any depth, and
[`IGNORED_RELPATHS_ON_COPY`](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.IGNORED_RELPATHS_ON_COPY) by path from the project root — which is
the only way to drop `artifacts/shots` while keeping `artifacts/audio`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.bench.corpus.html.md

# an.bench.corpus

The bench corpus: which projects are measured, and what each must actually render.

`expect_visual_kinds` is not belt-and-braces. Every `examples/*/assets/` is
gitignored, and before an#33 a missing character descriptor made the compiler
fall back to the procedural rig with **zero** warnings. The first
cross-architecture capture measured exactly that: three CI runners agreed
perfectly about a picture that was not the picture, and the agreement read as a
clean positive result. It surfaced only because the local machine happened to
hold a *stale* build product and therefore disagreed.

So a fixture declares the render path it must exercise, and the check reads the
scene JSON **the browser actually loaded** — an independent second opinion to
`strict_assets=True`, which trusts the compiler that produced it.

### Module Attributes

| [`SHOT_DIR_GLOB`](_autosummary/an.bench.corpus.html.md#an.bench.corpus.SHOT_DIR_GLOB)   | Per-shot subdirectory naming inside `.an/render_work`, and the staged scene filename.                                                                                             |
|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DFLT_FIXTURES`](_autosummary/an.bench.corpus.html.md#an.bench.corpus.DFLT_FIXTURES)   | The cut-out scenes of the original corpus moved to `cutan` (`cutan.bench.CUTOUT_FIXTURES`, an#225); this is the core's: `prop_swap` and the core corpus (`an.bench.core_corpus`). |

### Functions

| [`assert_render_path`](_autosummary/an.bench.corpus.html.md#an.bench.corpus.assert_render_path)(name, fixture, kinds)   | Refuse a capture that did not exercise the path its fixture declares.   |
|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`iter_shot_dirs`](_autosummary/an.bench.corpus.html.md#an.bench.corpus.iter_shot_dirs)(work_dir, \*, order)        | `(shot_id, shot_dir)` for every rendered shot, in **timeline** order.   |
| [`staged_scene`](_autosummary/an.bench.corpus.html.md#an.bench.corpus.staged_scene)(shot_dir)                     | The compiled scene JSON the browser actually loaded, for one shot.      |
| [`visual_kinds`](_autosummary/an.bench.corpus.html.md#an.bench.corpus.visual_kinds)(scene_json)                   | Every `visual.kind` in a staged scene's node tree.                      |

### Exceptions

| [`CorpusError`](_autosummary/an.bench.corpus.html.md#an.bench.corpus.CorpusError)   | A fixture did not render what it declared.   |
|----------------------------------------------------------------|----------------------------------------------|

### *exception* an.bench.corpus.CorpusError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A fixture did not render what it declared.

### an.bench.corpus.DFLT_FIXTURES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Fixture](_autosummary/an.bench.core_corpus.html.md#an.bench.core_corpus.Fixture)]* *= {'after_plane': Fixture(path='misc/bench/corpus/after_plane', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="a prop placed BETWEEN two planes under a pan (an#344): a far sky (depth 0.25), a disc with \`stage.after: set/sky\`, then a wall in two pieces with a window gap and a sill, all at depth 1.0, while the camera pans 60 px. The disc is drawn behind the walls and rides the sky's parallax through its band wrapper: between the goldens the walls move 40 px and the sky and disc 10 px. A regression in the draw order (the disc over the wall), in the wrapper's compensation (the disc sliding against the sky), or in the band containers' addressing moves a golden."), 'front_plane': Fixture(path='misc/bench/corpus/front_plane', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="an environment cut by \`characters_after\` with an ANIMATED foreground plane (an#343): \`stage\` draws \`sky\` and \`hill\` behind a world text label and \`rail\` in front of it, in its own container with \`scope: stage\`, so the rail is addressed \`stage/rail\` (never \`stage_\_front/rail\`) and its \`y\` tween rises from 60 to 0 over the label. What moves between the goldens is the rail crossing the label: a regression in the runtime's scoped indexing (the channel would name nothing and the load throws), in the cut, or in the draw order moves a golden."), 'path_draw': Fixture(path='misc/bench/corpus/path_draw', prepare=None, expect_visual_kinds=frozenset({'path'}), golden_frames=(0.0, 0.3333333333333333), golden_note="two stroked paths (an#160, an#161), both dashed and both coloured by a StylePack's \`stroke\` role: a marching-ants frame whose \`dash_offset\` runs 0 -> 20 px, and a cubic arrow that draws itself on (\`trim_end\` 0 -> 1) with its head on the moving tip. What moves between the goldens is the ROUTE growing (frame 0 shows none of it) and the frame's dashes sliding 6.7 px along their path; a regression in trim, in the dash phase, in the anchored-at-the-path-start rule that keeps a dash from crawling as the tip advances, or in the pack reaching a path, moves a golden. Butt caps, so a dash's ends are exact rather than rounded past their length."), 'path_shape': Fixture(path='misc/bench/corpus/path_shape', prepare=None, expect_visual_kinds=frozenset({'path'}), golden_frames=(0.0, 0.3333333333333333), golden_note='closed and filled paths (an#161): a badge drawn whole (fill under a mitred border joined at its first corner, no caps there), and a region as two entities, a border-less fill whose node \`alpha\` fades 0 -> 1 under a closed border that draws itself on (\`trim_end\` 0 -> 1). Frame 0 shows only the badge; a regression in the fill, the closing join, the fill staying untrimmed or a \`width: 0\` path drawing a stroke moves a golden.'), 'path_taper': Fixture(path='misc/bench/corpus/path_taper', prepare=None, expect_visual_kinds=frozenset({'path'}), golden_frames=(0.0, 0.3333333333333333), golden_note="variable-width paths (an#161), drawn as filled outlines: a wobbled brush stroke swelling from nothing to 1.2x and back to a point, and an arrow widening 0.2x -> 1x along a mitred polyline that draws itself on with its head on the tip. The width is anchored to the WHOLE path, so the arrow's base does not thin as it grows; a regression in the profile, the miter, the anchoring or the fill-instead-of-stroke rule moves a golden."), 'path_wobble': Fixture(path='misc/bench/corpus/path_wobble', prepare=None, expect_visual_kinds=frozenset({'path'}), golden_frames=(0.0, 0.3333333333333333), golden_note='two hand-drawn paths (an#161): a solid frame wobbling 3 px at a 60 px wavelength, and an arrow wobbling 4 px that draws itself on (\`trim_end\` 0 -> 1). The wobble is compile-time (seeded by the entity id, no trigonometry), so the frame is the same in both goldens and only the route grows; a change to the noise, the seed, the end envelope or the resampling moves both.'), 'prop_swap': Fixture(path='misc/bench/corpus/prop_swap', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.375), golden_note="a two-state prop swapping mid-shot (an#108): a desk lamp whose \`lamp\` asset-set goes \`off\` -> \`on\` at t=0.25. What moves between the goldens is a texture SWAP and nothing else — no transform, no easing, no interpolation — which is why this scene is worth a row the other seven cannot provide: every one of them measures a pose changing continuously, so a regression that broke swap resolution alone (the runtime resolves two swap properties on one node by NAME order, and an#87's failure mode was keeping the PREVIOUS texture in silence) would move no golden anywhere in the corpus. Frame 9 rather than the mid-frame: at 24 fps the swap lands on frame 6, so frame 9 is clear of the boundary in a way that does not depend on how the frame containing t=0.25 rounds."), 'rig_chain': Fixture(path='misc/bench/corpus/rig_chain', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="a desk-lamp prop with \`nesting: bones\` (an#340): base -> upper arm -> forearm, and a shade NESTED on the forearm's bone (the sword in the hand). The upper arm (rest 30 deg) tweens 0.52 -> -0.3 rad, the forearm (rest -70 deg, relative to the upper arm) -1.22 -> -0.4, the shade 0 -> 0.6: each turns about its joint and carries what hangs from it. A regression that flattened the chain (forearm left behind by the upper arm), inherited a parent's attachment offset, or applied a rest pose twice moves a golden."), 'rig_interleave': Fixture(path='misc/bench/corpus/rig_interleave', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="the rig_chain desk lamp (\`nesting: bones\`, base -> upper arm -> forearm -> shade) with an UNRELATED panel ordered between the upper arm (1) and the forearm (3) (an#430): the panel covers the upper arm's top and the forearm crosses OVER the panel, which no sort of one container's items can give. Painted from the entity's global \`paint_order\`. The same three tweens as rig_chain move the arm between the goldens. A regression that painted the chain together (the panel over both arms, or under both) moves a golden."), 'rig_order': Fixture(path='misc/bench/corpus/rig_order', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="a nested chain drawn BEHIND the part it hangs from (an#403): \`rig_chain\`'s desk lamp in \`nesting: bones\` with the whole arm (upper, forearm, shade: draw orders 1-3) behind its base (4). The base's container sorts its own drawing after the arm (\`sortableChildren\`, \`z_index\`), which the stage used to refuse. Between the goldens the arm swings as in \`rig_chain\`; where it passes over the base the base is in front. A regression in the runtime's sort, the compiler's \`z_index\` or the chain refusal moves a golden."), 'rig_origin': Fixture(path='misc/bench/corpus/rig_origin', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="two copies of one three-bone signpost rig (base, post, sign) at the same \`stage.at\` height (an#338): \`footed\` declares its \`origin\` at the foot of its base, so its foot stands ON the placement line; \`centred\` declares none, so the middle of its bones' extent lands there and it hangs lower. \`footed\` tweens \`rotation\` 0 -> -0.4 rad, which turns it about the declared origin: between the goldens its sign swings left while its foot does not move, and \`centred\` does not move at all. A regression that ignored \`origin\` (both posts at one height), placed parts about the wrong point, or broke the shared rig builder moves a golden."), 'rig_rest': Fixture(path='misc/bench/corpus/rig_rest', prepare=None, expect_visual_kinds=frozenset({'svg_sprite'}), golden_frames=(0.0, 0.3333333333333333), golden_note="a tripod prop whose three legs are ONE drawing on three bones with \`rotation_deg\` 22 / 0 / -22 (an#339): the splay is the bones' rest pose, not pixels. The whole tripod tweens \`rotation\` 0 -> 0.3 rad about its declared origin (the centre foot), and the splayed legs ride it: at frame 8 the rig is tilted 0.2 rad with the splay intact. A regression that dropped the rest pose (three parallel legs), applied it twice, or let the entity's rotation replace a leg's instead of composing with it moves a golden."), 'stage_pan': Fixture(path='misc/bench/corpus/stage_pan', prepare=None, expect_visual_kinds=frozenset({'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="three coloured blocks at depths 0.25 / 1.0 / 2.0 under a zoom-free pan (an#111). What moves between the goldens is the SEPARATION: the blocks start aligned and end 10 / 40 / 80 px apart, which is the parallax and nothing else. Frame 8, not the mid-frame: the camera travels 5 px per frame and the far plane moves a quarter of that, so only every fourth frame lands every block on an exact pixel boundary — at any other frame the anti-aliased edge changes the exact-colour mask's SIZE, and a centroid measured against a different shape is not a displacement (the measurement refuses it outright). Zoom is held constant on purpose: the x = 0 probe that cancels it in the JSON half does not reach a centroid, which sits at the plane's own offset."), 'text_card': Fixture(path='misc/bench/corpus/text_card', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="text under a camera push-in and roll (an#279, the core corpus): two OVERLAY words fading in one after the other (\`word_1\` starts 0.125 s after \`word_0\`) and a WORLD label on a plane block, while the camera zooms 1.0 -> 1.3 and rolls 0.12 rad. What moves between the goldens: the words' alpha (frame 0 shows neither), the label and block growing and turning with the camera, and the overlay title NOT turning — a regression that put overlay text in the world, broke per-word addressing or the camera's zoom/roll moves a golden. The face is Pillow's embedded Aileron, so the glyphs do not depend on the machine's fonts."), 'text_counter': Fixture(path='misc/bench/corpus/text_counter', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.5), golden_note="a calendar counting 1 to 30 with ONE text block (an#342): a right-aligned \`counter: {format: '{d}', start: 1}\` under one linear \`tween day value -> 30\` over 1 s, lowered at compile to a replacement set of the strings the frames show. Frame 0 shows 1; frame 12's value is exactly 15.5, which nearest-half-even rounding shows as 16 (15 would mean ties away from even, 15/17 a sampling or set-time shift). A regression in the sampling grid, the half-frame set time, the rounding or the right-edge geometry moves a golden."), 'text_outline': Fixture(path='misc/bench/corpus/text_outline', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note="OUTLINED text (an#313, OverSimplified's labels): white words with a 4 px near-black outline over a mid-blue plate and a pale stripe, the second word fading in from 0.125 s. Frame 0 shows the first word alone, outline and all, and NO outline where the second word will be (the outline lives in the word's own texture, so its alpha hides both); frame 8 shows both, legible over the blue and the stripe alike. A regression in the outline's width, its order under the fill, the box growth that keeps it unclipped, or per-word alpha moves a golden."), 'text_swap': Fixture(path='misc/bench/corpus/text_swap', prepare=None, expect_visual_kinds=frozenset({'svg_sprite', 'rect'}), golden_frames=(0.0, 0.3333333333333333), golden_note='a text block\\'s CONTENT changing within one shot (an#341): one right-aligned \`unit: block\` label whose \`texts\` set is swapped twice, "Day 1" -> "Day 12" at 0.125 s (the entity-level \`set day text d12\`) -> "Day 300" at 0.25 s (the \`day/block_0\` path). What moves between the goldens is the string, growing LEFTWARDS from a fixed right edge: a regression in the swap set, in the per-key geometry anchored on the \`align\` edge, or in the entity-level fan-out moves a golden.'), 'transitions': Fixture(path='misc/bench/corpus/transitions', prepare=None, expect_visual_kinds=frozenset({'path', 'rect'}), golden_frames=(0.08333333333333333, 0.375, 0.625), golden_note="the delivered film's COMPOSED frames (an#279, the core corpus): \`dusk\` fades in from black over 0.25 s, then \`dawn\` dissolves in over 0.25 s (frames 6-11 are the blend; the film is 12 + 12 - 6 = 18 frames). Frame 2 is mid-fade, frame 9 mid-dissolve (both pictures at once), frame 15 \`dawn\` alone with its arrow. A regression in the fade colour, the dissolve weights, the overlap arithmetic or the order of the shots moves a golden. The only fixture measured on the film's frames rather than the shots' — what the delivered mp4 shows (an.bench.capture's film segment).")}*

The cut-out scenes of the original corpus moved to `cutan` (`cutan.bench.CUTOUT_FIXTURES`, an#225); this
is the core’s: `prop_swap` and the core corpus (`an.bench.core_corpus`).

### an.bench.corpus.SHOT_DIR_GLOB *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'shot_\*'*

Per-shot subdirectory naming inside `.an/render_work`, and the staged scene
filename. Mirrored from the renderer rather than restated as literals at
each use site.

### an.bench.corpus.assert_render_path(name, fixture, kinds)

Refuse a capture that did not exercise the path its fixture declares.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.bench.corpus.iter_shot_dirs(work_dir, , order)

`(shot_id, shot_dir)` for every rendered shot, in **timeline** order.

`order` is mandatory, and that is the whole point of this signature.
`an/render.py` concatenates `[r.mp4_path for r in shot_results]` built
from `list(scene.timeline)`, while this function’s previous form returned
`sorted(work_dir.glob("shot_*"))` — directory-name order. The two agree
only when the shot ids happen to sort into timeline order, and when they do
not, every encode-side metric pairs source frame *i* of one shot against
decoded frame *i* of another. The `multi_shot` fixture’s ids (`intro`
then `beat`) are chosen so they disagree.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]]

```pycon
>>> import tempfile
>>> from pathlib import Path
>>> d = Path(tempfile.mkdtemp())
>>> for name in ("shot_intro", "shot_beat"): (d / name).mkdir()
>>> [i for i, _ in iter_shot_dirs(d, order=["intro", "beat"])]
['intro', 'beat']
```

### an.bench.corpus.staged_scene(shot_dir)

The compiled scene JSON the browser actually loaded, for one shot.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.corpus.visual_kinds(scene_json)

Every `visual.kind` in a staged scene’s node tree.

Read from the staged file rather than re-compiled, so it reports what was
rendered rather than what a second compile would produce.

Scoped to `scene` and to the `visual` key specifically. A sweep for
every `kind` anywhere in the document — which is what this was before —
also collects an#33’s `asset_resolution` entries, whose `kind` is
`"character"` / `"environment"`. Those are entity kinds, not visual
kinds, and mixing them makes the field mean two things at once.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> sorted(visual_kinds({"scene": {"visual": {"kind": "rect"},
...                                "children": [{"visual": {"kind": "eye"}}]},
...                      "asset_resolution": [{"kind": "character"}]}))
['eye', 'rect']
```


# _autosummary/an.bench.environment.html.md

# an.bench.environment

The environment tuple — the fields that decide whether two rows may be compared.

Split into two halves with **different comparison rules**, because the two
sides of the pipeline were measured and their answers are opposite:

**Render side — comparable on any machine.** Both render paths, four machines
(local arm64 macOS, `macos-latest`, `ubuntu-latest` x86-64,
`ubuntu-24.04-arm`), 132 frames each: zero differing pixels *and* zero
differing PNG bytes, across two different SwiftShader JIT backends. So the
render-side metrics need **no cross-machine band column**, and the golden
corpus can be a CI gate. Record the Chromium build and the \*\*launch argv
verbatim\*\* — all four rasteriser configurations report a byte-identical
`UNMASKED_RENDERER_WEBGL` string, so the renderer string is demonstrably
blind to the choice it was proposed to guard.

**Encode side — machine-scoped, not bandable.** Same ISA + same x264 build is
byte-identical; a different ISA moves the decoded stream a little (luma <=2.66%
of samples); a different x264 build moves it by two orders of magnitude (up to
99.2% of samples, mean 

```
|d|
```

 3.94, max 36). A band that wide would swallow
`flat_field_deviation`’s entire crf18->23 signal. So `--compare` (an#40)
must **refuse** rows whose `x264_sei` or `isa` differ, in the same way it
refuses rows with a different `scene_contract_sha256` — the number is
uninterpretable, not good or bad.

### Module Attributes

| [`RUNTIME_DIGEST_SUFFIXES`](_autosummary/an.bench.environment.html.md#an.bench.environment.RUNTIME_DIGEST_SUFFIXES)   | `_stage_job`'s `shutil.copytree` is a bare copy that DOES deliver everything under the runtime dir into the staged tree, so "what gets staged" cannot be the reason to exclude anything — the real reason is "what the page loads": `index.html`/`preview.html` pull in `.js` (including `vendor/`), `.css` and `.json`, and nothing else under the tree is read by the browser (an#141).   |
|----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`RUNTIME_IGNORED_SUFFIXES`](_autosummary/an.bench.environment.html.md#an.bench.environment.RUNTIME_IGNORED_SUFFIXES)  | Suffixes under the runtime dir that are known to be non-runtime — excluded from the digest on purpose, not by omission.                                                                                                                                                                                                                                                                     |

### Functions

| [`environment_record`](_autosummary/an.bench.environment.html.md#an.bench.environment.environment_record)(\*, pix_fmt[, x264_sei, ...])   | Everything about this machine that could plausibly move a number.            |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`ffmpeg_identity`](_autosummary/an.bench.environment.html.md#an.bench.environment.ffmpeg_identity)()                                  | The ffmpeg build banner.                                                     |
| [`home_relative`](_autosummary/an.bench.environment.html.md#an.bench.environment.home_relative)(path, \*[, home])                    | `path` with the user's home directory spelled `~`.                           |
| [`probe_browser`](_autosummary/an.bench.environment.html.md#an.bench.environment.probe_browser)()                                    | Launch Chromium with the render path's own flags and read back its identity. |
| [`runtime_sha256`](_autosummary/an.bench.environment.html.md#an.bench.environment.runtime_sha256)()                                   | A digest of the JS runtime the renderer will stage, files and names.         |
| `tool_version`(name)                                                                                |                                                                              |
| [`x264_sei`](_autosummary/an.bench.environment.html.md#an.bench.environment.x264_sei)(mp4)                                      | The encoder build + thread count, read straight out of the file.             |

### an.bench.environment.RUNTIME_DIGEST_SUFFIXES *= ('.js', '.html', '.css', '.json')*

`_stage_job`’s
`shutil.copytree` is a bare copy that DOES deliver everything under the
runtime dir into the staged tree, so “what gets staged” cannot be the
reason to exclude anything — the real reason is “what the page loads”:
`index.html`/`preview.html` pull in `.js` (including `vendor/`), `.css` and
`.json`, and nothing else under the tree is read by the browser (an#141).
Package metadata (`__init__.py`), docs (`README.md`), vendor licence text
can change — a stray `.DS_Store` included — without the page the renderer
loads changing at all, and hashing them anyway made the digest answer “did
the runtime change” wrong.

* **Type:**
  Suffixes `runtime_sha256` hashes. Deliberately narrow

### an.bench.environment.RUNTIME_IGNORED_SUFFIXES *= ('', '.py', '.pyc', '.md', '.txt')*

Suffixes under the runtime dir that are known to be non-runtime — excluded
from the digest on purpose, not by omission. `""` covers extension-less
dotfiles (`.DS_Store`, the one this issue was filed over); `.pyc` covers a
stray `__pycache__/*.pyc` from `__init__.py` (the issue’s other named
example — reproducible on this very tree). `test_bench_environment.py`
fails on any suffix that lands in neither this set nor
`RUNTIME_DIGEST_SUFFIXES`, so a future runtime asset (an `.svg`, `.woff`,
`.wasm`, `.mjs`) cannot silently fall outside what the digest sees (an#141).

### an.bench.environment.environment_record(, pix_fmt, x264_sei=None, browser=None)

Everything about this machine that could plausibly move a number.

`browser` lets the caller supply an already-taken probe. The golden gate
needs the Chromium build *before* the run-level provenance is assembled —
the path keys on it — and probing twice would launch a second browser and
could, in principle, report a different build from the one that rendered.

`pix_fmt` is required and has no default, because it is a \*\*comparability
key\*\* and the caller is the only one who can measure it. It must be the
format the delivered files actually are — `imageio.delivered_pix_fmt` — and
not a re-derivation, for the reason an#72 records: the delivered encode
resolves its format from `RenderContext.pix_fmt` *or* the module global,
so reading either one is a second source of truth that can disagree with
the file, and this field is what `bench-compare` uses to decide whether two
encode-side rows may be compared at all.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.bench.environment.ffmpeg_identity()

The ffmpeg build banner. Informational — the `x264_sei` is the key.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.bench.environment.home_relative(path, , home=None)

`path` with the user’s home directory spelled `~`.

Ledger rows are committed to a public repository, so a row must not carry
the absolute path of the machine that wrote it. Provenance only: nothing
compares on this field. POSIX-spelled on every platform.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> home_relative("/home/u/.cache/ms-playwright/chrome", home=Path("/home/u"))
'~/.cache/ms-playwright/chrome'
>>> home_relative("/opt/chrome", home=Path("/home/u"))
'/opt/chrome'
```

### an.bench.environment.probe_browser()

Launch Chromium with the render path’s own flags and read back its identity.

Never raises: a probe that crashes must not cost a caller a completed
capture, and a recorded `error` is more honest than a missing field that
reads as “nothing to report”.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.bench.environment.runtime_sha256()

A digest of the JS runtime the renderer will stage, files and names.

Provenance, NOT a comparability key: the runtime is the code under test, and
two rows rendered by different runtimes are exactly what `an bench --compare`
exists to compare. What it buys is that a render-side mutation leaves a
fingerprint in the row — before this, the `disabled_aa` lever had no way to
prove it applied, and `assert not report["mutation_may_not_have_applied"]`
asserted nothing for it (an#41 review).

Only files whose suffix is in `RUNTIME_DIGEST_SUFFIXES` are hashed, so a
file that is not staged as a runtime asset — a stray `.DS_Store`, a
`__pycache__` entry, package metadata — cannot move the digest (an#141).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.bench.environment.x264_sei(mp4)

The encoder build + thread count, read straight out of the file.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.bench.golden.html.md

# an.bench.golden

The golden gate: committed frames, compared on **decoded pixels**.

Family B of the panel. Everything else in [`an.bench`](_autosummary/an.bench.html.md#module-an.bench) measures a property
of one render; this compares today’s render against a picture a human looked at
and blessed, so it is the only part that can catch a change nobody predicted.

Four decisions here that are easy to get wrong in a way that still looks like
it works:

\*\*The criterion is `sha256` of the decoded RGB array, never the file bytes.\*\*
Chromium 1187 -> 1223 changed 144 of 144 PNG files and **zero** pixels. A
file-byte gate goes red on the first Playwright bump for a reason that has
nothing to do with animation quality — and, worse, trains people to re-bless
without looking.

**The path keys on the Chromium build ALONE** — no platform, no arch. Measured
across arm64 macOS, x86-64 Linux and arm64 Linux, across two different
SwiftShader JIT backends: zero differing pixels *and* zero differing PNG bytes.
Carrying the platform would force one committed copy per platform for no
information. What the convention keeps is its real benefit: a Playwright bump
becomes a **new path requiring a deliberate re-bless**, not a red test with no
explanation.

**Three different absences get three different gates.** “This scene declares no
golden frames”, “goldens exist but not for this Chromium build” and “the
Chromium build could not be determined at all” are three different facts, and a
reader who cannot tell them apart cannot act on any of them. The last one
matters most: [`an.bench.environment.probe_browser()`](_autosummary/an.bench.environment.html.md#an.bench.environment.probe_browser) never raises, it
returns `{"error": ...}` — so without a distinct gate an un-probeable browser
reads exactly like a scene nobody has blessed yet.

**A comparison against a golden written in the same run is a tautology**, so a
`--bless` run records family B as gated rather than as a pass. The row would
otherwise carry a perfect score that no code could ever have failed.

### Module Attributes

| [`BLESS_MANIFEST_TEMPLATE`](_autosummary/an.bench.golden.html.md#an.bench.golden.BLESS_MANIFEST_TEMPLATE)   | Per-scene, per-build bless record, committed beside the frames.                         |
|----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| [`REQUIRED_GOLDEN_FRAMES`](_autosummary/an.bench.golden.html.md#an.bench.golden.REQUIRED_GOLDEN_FRAMES)    | How many golden frames a scene must declare.                                            |
| [`GATE_UNDECLARED`](_autosummary/an.bench.golden.html.md#an.bench.golden.GATE_UNDECLARED)           | Gate names.                                                                             |
| [`RETIRED_GATES`](_autosummary/an.bench.golden.html.md#an.bench.golden.RETIRED_GATES)             | Gate names that appear in rows written BEFORE this module existed, and what they meant. |

### Functions

| [`bless_scene`](_autosummary/an.bench.golden.html.md#an.bench.golden.bless_scene)(capture, \*, times, ...[, ...])       | Write one scene's golden frames and its bless record.                         |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`chromium_build_of`](_autosummary/an.bench.golden.html.md#an.bench.golden.chromium_build_of)(environment)                    | The Chromium build from an environment record, or `None` if unknown.          |
| [`compare_scene`](_autosummary/an.bench.golden.html.md#an.bench.golden.compare_scene)(capture, \*, times, chromium_build) | Compare today's render against the committed goldens for one scene.           |
| [`frame_index_for`](_autosummary/an.bench.golden.html.md#an.bench.golden.frame_index_for)(time, \*, fps, n_frames)          | The frame a pinned time names, snapped to the nearest one.                    |
| [`frame_key`](_autosummary/an.bench.golden.html.md#an.bench.golden.frame_key)(index)                                  | The filename stem for a frame, zero-padded so a directory listing sorts.      |
| [`frame_png_path`](_autosummary/an.bench.golden.html.md#an.bench.golden.frame_png_path)(capture, ref)                      | Where the renderer left the PNG for one resolved frame.                       |
| [`iter_committed`](_autosummary/an.bench.golden.html.md#an.bench.golden.iter_committed)(scene, chromium_build, \*[, root]) | Every committed golden PNG for one scene and build, in sorted order.          |
| [`load_manifest`](_autosummary/an.bench.golden.html.md#an.bench.golden.load_manifest)(scene, chromium_build, \*[, root])  | The committed bless record, or `None` when this scene has never been blessed. |
| [`manifest_path`](_autosummary/an.bench.golden.html.md#an.bench.golden.manifest_path)(scene, chromium_build, \*[, root])  | Where one scene's bless record for one Chromium build lives.                  |
| [`pixels_sha256`](_autosummary/an.bench.golden.html.md#an.bench.golden.pixels_sha256)(rgb)                                | `sha256` over the decoded pixels — **shape and dtype included**.              |
| [`resolve_frames`](_autosummary/an.bench.golden.html.md#an.bench.golden.resolve_frames)(capture, times)                    | Map each pinned time onto `(global index, shot, index within that shot)`.     |

### Classes

| [`FrameRef`](_autosummary/an.bench.golden.html.md#an.bench.golden.FrameRef)(key, time, index, shot_id, local_index)   | One pinned golden frame, resolved against the render that just happened.   |
|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|

### Exceptions

| [`GoldenError`](_autosummary/an.bench.golden.html.md#an.bench.golden.GoldenError)   | A bless was refused, or a committed golden is unusable.   |
|----------------------------------------------------------------|-----------------------------------------------------------|

### an.bench.golden.BLESS_MANIFEST_TEMPLATE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'bless-chromium{chromium_build}.json'*

Per-scene, per-build bless record, committed beside the frames. Per build
rather than per scene: a Playwright bump adds a new set of frames under new
names, and the old set stays valid for anyone still on the old build.

### *class* an.bench.golden.FrameRef(key, time, index, shot_id, local_index)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One pinned golden frame, resolved against the render that just happened.

#### local_index *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Index WITHIN that shot’s frame directory.

### an.bench.golden.GATE_UNDECLARED *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'golden_frames_undeclared'*

Gate names. Literals rather than an enum because they are written into the
ledger and read back by `an bench --compare` from rows written by older
registries, so their spelling is a wire format.

### *exception* an.bench.golden.GoldenError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A bless was refused, or a committed golden is unusable.

### an.bench.golden.REQUIRED_GOLDEN_FRAMES *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 2*

How many golden frames a scene must declare. Two, because one frame tests a
single instant and cannot notice a scene that renders its first frame
correctly and then stops.

### an.bench.golden.RETIRED_GATES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'golden_absent': 'golden_absent_for_chromium_build'}*

Gate names that appear in rows written BEFORE this module existed, and what
they meant. `an bench --compare` (an#40) reads old rows as fact, so the one
place a reader has to look is here rather than in a commit message. The
an#36 row carried a single `golden_absent` for both family-B keys, at a time
when the corpus had no goldens for ANY build — which is this module’s
`GATE_ABSENT`, not its `GATE_UNDECLARED` (the fixtures did declare no times,
but the gate was not distinguishing the two).

### an.bench.golden.bless_scene(capture, , times, chromium_build, reason, git, scene_contract_sha256, golden_note='', root=None)

Write one scene’s golden frames and its bless record. Refuses, loudly.

Every refusal here is a case where writing the file would produce a gate
that cannot fail:

- **no reason** — a re-bless with no recorded reason is the same failure as
  a silently widened threshold, which is the named failure mode this whole
  wave exists to prevent;
- **fewer than two frames** — one frame cannot notice a scene that renders
  its first instant correctly and then stops;
- **a pixel-identical pair** — measured on `promote_demo`: frame 0 and the
  `duration/2` frame differ by exactly zero pixels, so the obvious choice
  blesses one picture twice and the second golden tests nothing;
- **an unknown Chromium build** — the path keys on it, so without it the
  frames would be written under a name no future run could look up.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.golden.chromium_build_of(environment)

The Chromium build from an environment record, or `None` if unknown.

`probe_browser` never raises — it reports an `error` key — so “we could
not ask the browser” arrives here as a missing field rather than as an
exception, and must not be confused with “nobody has blessed this scene”.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.bench.golden.compare_scene(capture, , times, chromium_build, root=None)

Compare today’s render against the committed goldens for one scene.

Returns a dict with `state` in `{"measured", "gated", "unavailable"}`,
the reduced numbers the ledger carries, and a per-frame record for
provenance. The reduction is **worst frame wins**: `identical` is the
conjunction and `min_ssim_win8` is the minimum, because a maximum or a
mean lets one clean frame hide a broken one, and this metric’s own name is
“the worst small window”.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.golden.frame_index_for(time, , fps, n_frames)

The frame a pinned time names, snapped to the nearest one.

`int(time * fps)` is the trap: `0.25 * 24` is `5.999999999999999` in
binary floating point, so the obvious spelling silently picks frame 5.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> frame_index_for(0.25, fps=24, n_frames=12)
6
>>> frame_index_for(0.0, fps=24, n_frames=12)
0
```

### an.bench.golden.frame_key(index)

The filename stem for a frame, zero-padded so a directory listing sorts.

Keyed on the frame **index**, not the pinned time: the index is what names
the picture. A change to `fps` moves the index, which changes the path,
which makes the golden absent and therefore *gated* — loud, and pointing at
the right cause.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> frame_key(7)
'f0007'
```

### an.bench.golden.frame_png_path(capture, ref)

Where the renderer left the PNG for one resolved frame.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.bench.golden.iter_committed(scene, chromium_build, , root=None)

Every committed golden PNG for one scene and build, in sorted order.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### an.bench.golden.load_manifest(scene, chromium_build, , root=None)

The committed bless record, or `None` when this scene has never been blessed.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.bench.golden.manifest_path(scene, chromium_build, , root=None)

Where one scene’s bless record for one Chromium build lives.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.bench.golden.pixels_sha256(rgb)

`sha256` over the decoded pixels — **shape and dtype included**.

Never over file bytes, for the reason in the module docstring. And never
over the raw buffer alone either: `tobytes()` carries no shape, so a
320x240 frame and a 240x320 one holding the same bytes hash identically,
and issue #38’s literal “the criterion is `sha256(decoded RGB array)`”
would report PASS on a transposed frame. Both orientations are live in this
corpus. [`an.bench.metrics.golden_comparison()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.golden_comparison) catches it separately via
its `shape_mismatch` branch; this makes the digest agree with the gate
rather than quietly disagreeing with it.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> import numpy as np
>>> a = np.arange(24, dtype=np.uint8)
>>> pixels_sha256(a.reshape(2, 4, 3)) == pixels_sha256(a.reshape(4, 2, 3))
False
```

### an.bench.golden.resolve_frames(capture, times)

Map each pinned time onto `(global index, shot, index within that shot)`.

Indices run over the scene’s **concatenated** timeline — what the delivered
mp4 shows — so a pinned time can land in the second shot, which is exactly
what `multi_shot`’s second golden does.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`FrameRef`](_autosummary/an.bench.golden.html.md#an.bench.golden.FrameRef)]


# _autosummary/an.bench.html.md

# an.bench

`an bench` — render a fixed corpus, compute a metrics panel, write one ledger row.

The instrument Wave 2 exists to build. Its job is not to say whether the
animation is good; it is to make a \*\*deliberate degradation move a number in a
direction declared in advance\*\*, so that a future regression is caught by
something other than someone noticing.

Three things about the panel that are easy to assume wrongly:

- **It is not the epic’s metric list.** All twelve originally-proposed metrics
  were refuted; every one here is a corrected form. `mean adjacent-frame
  SSIM` in particular is *out* — it moves the wrong way (0.958 at crf18 ->
  0.977 at crf51, because a crushed video is smoother), so shipping it would
  put a number in the ledger that rewards the degradation the gate exists to
  catch.
- \*\*Render-side and encode-side metrics are blind to each other’s mutations by
  construction\*\*, and are labelled so nothing mixes them. Render-side rows
  compare across any machine; encode-side rows are machine-scoped.
- **The vision judge is deliberately not here.** Not because its input is
  nondeterministic — over frozen frames it is perfectly reproducible — but
  because a cassetted judge is a *constant*, invariant to the code under test,
  so it can never move under a deliberate degradation.

Entry points: [`an.bench.run.run_bench()`](_autosummary/an.bench.run.html.md#an.bench.run.run_bench), and `an bench` on the CLI.

### Functions

| [`bless_scene`](_autosummary/an.bench.html.md#an.bench.bless_scene)(capture, \*, times, ...[, ...])       | Write one scene's golden frames and its bless record.                         |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`compare_scene`](_autosummary/an.bench.html.md#an.bench.compare_scene)(capture, \*, times, chromium_build) | Compare today's render against the committed goldens for one scene.           |
| [`decode_png`](_autosummary/an.bench.html.md#an.bench.decode_png)(data)                                  | Decode an 8-bit truecolour PNG to `(H, W, C)` uint8, `C` in `{3, 4}`.         |
| [`encode_png`](_autosummary/an.bench.html.md#an.bench.encode_png)(rgb, \*[, level])                      | Encode `(H, W, 3)` uint8 as an 8-bit truecolour PNG, every row filter 0.      |
| [`frame_key`](_autosummary/an.bench.html.md#an.bench.frame_key)(index)                                  | The filename stem for a frame, zero-padded so a directory listing sorts.      |
| [`pixels_sha256`](_autosummary/an.bench.html.md#an.bench.pixels_sha256)(rgb)                                | `sha256` over the decoded pixels — **shape and dtype included**.              |
| [`png_dimensions`](_autosummary/an.bench.html.md#an.bench.png_dimensions)(data)                              | `(width, height)` from a PNG's IHDR, without decoding a single pixel.         |
| [`read_png`](_autosummary/an.bench.html.md#an.bench.read_png)(path)                                    | `(H, W, 3)` uint8 for a PNG on disk, alpha dropped only if opaque.            |
| [`read_png_dimensions`](_autosummary/an.bench.html.md#an.bench.read_png_dimensions)(path)                         | `(width, height)` for a PNG on disk, reading only its header.                 |
| [`write_png`](_autosummary/an.bench.html.md#an.bench.write_png)(path, rgb, \*[, level])                 | Write `rgb` as a filter-0 PNG and **verify the round trip** before returning. |
| [`build_ledger`](_autosummary/an.bench.html.md#an.bench.build_ledger)(\*, provenance, scenes)              | The whole row.                                                                |
| [`build_scene_block`](_autosummary/an.bench.html.md#an.bench.build_scene_block)(\*, provenance, metrics, ...)   | Assemble one scene's three blocks, refusing anything unreadable.              |
| [`witnesses`](_autosummary/an.bench.html.md#an.bench.witnesses)(ledger_scene, mutation)                 | Which metrics would count for `mutation`, grouped by family.                  |
| `run_bench`(\*[, scenes, out, keep_render, ...])                                                   | Render the corpus, compute the panel, and (by default) write the row.         |
| `format_panel`(ledger)                                                                             | A human-readable digest of a row — the thing `an bench` prints.               |

### Classes

| [`MetricSpec`](_autosummary/an.bench.html.md#an.bench.MetricSpec)(key, family, unit, optimum, ...)     | One row of the panel.                                                      |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`Prediction`](_autosummary/an.bench.html.md#an.bench.Prediction)(expect[, counts, gate, reason, ...]) | What one metric is expected to do under one mutation, declared in advance. |
| [`Value`](_autosummary/an.bench.html.md#an.bench.Value)(value[, state, gate, detail, extra])      | One measured (or deliberately absent) number.                              |

### Exceptions

| [`GoldenError`](_autosummary/an.bench.html.md#an.bench.GoldenError)       | A bless was refused, or a committed golden is unusable.             |
|--------------------------------------------------------------------|---------------------------------------------------------------------|
| [`PngFormatError`](_autosummary/an.bench.html.md#an.bench.PngFormatError)    | A PNG this module deliberately does not decode, or a malformed one. |
| [`RegistryError`](_autosummary/an.bench.html.md#an.bench.RegistryError)     | A metric declaration violates one of the table's invariants.        |
| [`LedgerSchemaError`](_autosummary/an.bench.html.md#an.bench.LedgerSchemaError) | A ledger row violates an invariant that would make it misreadable.  |
| `BenchError`                                                       | The bench could not produce a row it would be honest to file.       |

### *exception* an.bench.GoldenError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A bless was refused, or a committed golden is unusable.

### *exception* an.bench.LedgerSchemaError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A ledger row violates an invariant that would make it misreadable.

### *class* an.bench.MetricSpec(key, family, unit, optimum, predictions, sentence, role=None, reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=<factory>, sweep=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One row of the panel.

#### requires *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

What a scene must HAVE for this row to exist at all, or `""` when the
row applies to every scene (an#111).

The render-side panel rule is “nothing may be null on a real capture,
because a null render-side row is a blind panel”. That rule assumes
every metric could have been measured. `stage_min_plane_ratio_gap`
could not: a displacement ratio needs two planes moving at different
depths, and `single_character` has no planes at all — so its null is
structural, not a gap in the instrument.

Declared rather than hardcoded in the test, so the panel rule keeps
naming its own exceptions instead of a test file carrying a list the
registry does not know about.

#### sweep *: [Sweep](_autosummary/an.bench.registry.html.md#an.bench.registry.Sweep) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Set only on a hard-threshold counter (an#140); see `Sweep`.

### *exception* an.bench.PngFormatError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A PNG this module deliberately does not decode, or a malformed one.

Typed and specific on purpose: the alternative to refusing is returning a
plausible array, and a golden gate that compares a plausible array is worse
than one that does not run.

### *class* an.bench.Prediction(expect, counts=False, gate=None, reason='', reference=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What one metric is expected to do under one mutation, declared in advance.

`expect=None` means **gated**: the number is uninterpretable rather than
good or bad, and `gate` says why. It is not “no change”.

### *exception* an.bench.RegistryError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A metric declaration violates one of the table’s invariants.

### *class* an.bench.Value(value, state='measured', gate=None, detail='', extra=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One measured (or deliberately absent) number.

### an.bench.bless_scene(capture, , times, chromium_build, reason, git, scene_contract_sha256, golden_note='', root=None)

Write one scene’s golden frames and its bless record. Refuses, loudly.

Every refusal here is a case where writing the file would produce a gate
that cannot fail:

- **no reason** — a re-bless with no recorded reason is the same failure as
  a silently widened threshold, which is the named failure mode this whole
  wave exists to prevent;
- **fewer than two frames** — one frame cannot notice a scene that renders
  its first instant correctly and then stops;
- **a pixel-identical pair** — measured on `promote_demo`: frame 0 and the
  `duration/2` frame differ by exactly zero pixels, so the obvious choice
  blesses one picture twice and the second golden tests nothing;
- **an unknown Chromium build** — the path keys on it, so without it the
  frames would be written under a name no future run could look up.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.build_ledger(, provenance, scenes)

The whole row.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.build_scene_block(, provenance, metrics, tripwires)

Assemble one scene’s three blocks, refusing anything unreadable.

Completeness is enforced in both directions. A metric the registry declares
but the row omits is a silently narrower panel; a metric the row carries
but the registry does not declare has no family, no side and no predicted
direction, so nothing downstream can count it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.compare_scene(capture, , times, chromium_build, root=None)

Compare today’s render against the committed goldens for one scene.

Returns a dict with `state` in `{"measured", "gated", "unavailable"}`,
the reduced numbers the ledger carries, and a per-frame record for
provenance. The reduction is **worst frame wins**: `identical` is the
conjunction and `min_ssim_win8` is the minimum, because a maximum or a
mean lets one clean frame hide a broken one, and this metric’s own name is
“the worst small window”.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.decode_png(data)

Decode an 8-bit truecolour PNG to `(H, W, C)` uint8, `C` in `{3, 4}`.

Refuses — rather than approximates — 16-bit, palette, greyscale and
interlaced images, naming what it found.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> a = np.array([[[1, 2, 3], [4, 5, 6]]], np.uint8)
>>> np.array_equal(decode_png(encode_png(a)), a)
True
```

### an.bench.encode_png(rgb, , level=9)

Encode `(H, W, 3)` uint8 as an 8-bit truecolour PNG, every row filter 0.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> import numpy as np
>>> data = encode_png(np.zeros((2, 3, 3), np.uint8))
>>> data[:8] == PNG_SIGNATURE
True
>>> decode_png(data).shape
(2, 3, 3)
```

### an.bench.frame_key(index)

The filename stem for a frame, zero-padded so a directory listing sorts.

Keyed on the frame **index**, not the pinned time: the index is what names
the picture. A change to `fps` moves the index, which changes the path,
which makes the golden absent and therefore *gated* — loud, and pointing at
the right cause.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> frame_key(7)
'f0007'
```

### an.bench.pixels_sha256(rgb)

`sha256` over the decoded pixels — **shape and dtype included**.

Never over file bytes, for the reason in the module docstring. And never
over the raw buffer alone either: `tobytes()` carries no shape, so a
320x240 frame and a 240x320 one holding the same bytes hash identically,
and issue #38’s literal “the criterion is `sha256(decoded RGB array)`”
would report PASS on a transposed frame. Both orientations are live in this
corpus. [`an.bench.metrics.golden_comparison()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.golden_comparison) catches it separately via
its `shape_mismatch` branch; this makes the digest agree with the gate
rather than quietly disagreeing with it.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> import numpy as np
>>> a = np.arange(24, dtype=np.uint8)
>>> pixels_sha256(a.reshape(2, 4, 3)) == pixels_sha256(a.reshape(4, 2, 3))
False
```

### an.bench.png_dimensions(data)

`(width, height)` from a PNG’s IHDR, without decoding a single pixel.

Width first, matching the PNG header itself — and deliberately the opposite
order from [`read_png()`](_autosummary/an.bench.html.md#an.bench.read_png), which returns `(H, W, C)` like every other
array here. Getting it backwards produces a square-looking check that
passes on square frames only, so the order is asserted in the tests.

Cheap on purpose. The bench reads this for **every** frame on disk rather
than sampling one, because the failure it exists to catch — a render whose
frame size changed partway through — is exactly the one sampling misses.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> import numpy as np
>>> png_dimensions(encode_png(np.zeros((240, 320, 3), np.uint8)))
(320, 240)
```

### an.bench.read_png(path)

`(H, W, 3)` uint8 for a PNG on disk, alpha dropped only if opaque.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.bench.read_png_dimensions(path)

`(width, height)` for a PNG on disk, reading only its header.

A 1080p frame is megabytes and its declared size is in the first 24 of
them. Reading only those is what makes checking every frame of every shot
free rather than a second full decode of the corpus.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### an.bench.witnesses(ledger_scene, mutation)

Which metrics would count for `mutation`, grouped by family.

Reads the row rather than the registry, so an#41’s criterion is evaluated
against what was actually written down.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> witnesses({"metrics": {"m": {"family": "A",
...     "under_mutation": {"high_crf": {"counts": True}}}}}, "high_crf")
{'A': ['m']}
```

### an.bench.write_png(path, rgb, , level=9)

Write `rgb` as a filter-0 PNG and **verify the round trip** before returning.

The verification is not defensive noise: it is the only thing standing
between a bug in this module’s own encoder and a committed golden that
silently disagrees with the frame it was blessed from. Research §3 asks for
exactly this — “assert the round trip at bless time against the in-memory
screenshot pixels, so a bug in `an`’s own encoder cannot hide”.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### Modules

| [`capture`](_autosummary/an.bench.capture.html.md#module-an.bench.capture)         | Render one corpus fixture into a throwaway copy, and hand back its artifacts.                 |
|------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| [`compare`](_autosummary/an.bench.compare.html.md#module-an.bench.compare)         | `an bench --compare`: read two ledger rows, and **refuse when they are not comparable**.      |
| [`contract`](_autosummary/an.bench.contract.html.md#module-an.bench.contract)       | `scene_contract_sha256`: the fact that decides whether two rows are comparable.               |
| [`core_corpus`](_autosummary/an.bench.core_corpus.html.md#module-an.bench.core_corpus) | The core corpus: what `an` renders with no character and no genre (ADR 0001 decision 7).      |
| [`corpus`](_autosummary/an.bench.corpus.html.md#module-an.bench.corpus)           | The bench corpus: which projects are measured, and what each must actually render.            |
| [`environment`](_autosummary/an.bench.environment.html.md#module-an.bench.environment) | The environment tuple — the fields that decide whether two rows may be compared.              |
| [`golden`](_autosummary/an.bench.golden.html.md#module-an.bench.golden)           | The golden gate: committed frames, compared on **decoded pixels**.                            |
| [`imageio`](_autosummary/an.bench.imageio.html.md#module-an.bench.imageio)         | The pinned ffmpeg decodes, and the lossless leg every encode-side metric is measured against. |
| [`ledger`](_autosummary/an.bench.ledger.html.md#module-an.bench.ledger)           | The ledger: three blocks that must never be mixed, and the guards that keep them apart.       |
| [`masks`](_autosummary/an.bench.masks.html.md#module-an.bench.masks)             | Every mask the panel is computed over, and the recorded operator for each.                    |
| [`metrics`](_autosummary/an.bench.metrics.html.md#module-an.bench.metrics)         | The metric panel: pure numpy, no I/O, no subprocess.                                          |
| [`mutants`](_autosummary/an.bench.mutants.html.md#module-an.bench.mutants)         | The guard mutants: "I mutation-tested it" as a runnable artifact, not a claim.                |
| [`mutations`](_autosummary/an.bench.mutations.html.md#module-an.bench.mutations)     | The levers: deliberate, declared changes through seams the shipped code already has.          |
| [`palette`](_autosummary/an.bench.palette.html.md#module-an.bench.palette)         | Derive the set of colours a compiled shot declared — never hand-pin it.                       |
| [`paths`](_autosummary/an.bench.paths.html.md#module-an.bench.paths)             | Where the bench reads its corpus from and writes its ledger to.                               |
| [`png`](_autosummary/an.bench.png.html.md#module-an.bench.png)                 | A filter-0 PNG writer and a full-filter reader — numpy and stdlib only.                       |
| [`registry`](_autosummary/an.bench.registry.html.md#module-an.bench.registry)       | The metric declaration table — what each number is, and which way it should move.             |
| [`run`](_autosummary/an.bench.run.html.md#module-an.bench.run)                 | `run_bench`: render the corpus, compute the panel, write one ledger row.                      |
| [`stage`](_autosummary/an.bench.stage.html.md#module-an.bench.stage)             | Measuring a pan: does the stage move as planes, or as one rigid image?                        |


# _autosummary/an.bench.imageio.html.md

# an.bench.imageio

The pinned ffmpeg decodes, and the lossless leg every encode-side metric
is measured against.

\*\*The reference is the lossless encode, not the PNGs — and that is a
correction, made because CI caught the alternative.\*\* The obvious design reads
the source frames back with an explicit conversion
(`-vf scale=out_range=tv:out_color_matrix=bt709 -pix_fmt yuv444p`) and compares
the delivered mp4 against that. Measured against a mathematically lossless
(`-qp 0`) encode of the same frames, that conversion agrees **exactly** on
ffmpeg 8.1 (luma residual 0.0000, max 0) and **does not** on the Linux runner’s
older build (0.6290, max 5). A floor of 0.63 is 42% of `coded_luma_edge_error`’s
whole crf23 value, so on that build every encode-side luma number would have
been measuring a colour-conversion disagreement and reporting it as encoder
damage.

The fix is not a tolerance. It is to stop having a second conversion at all:
`-qp 0` is lossless, so \*\*the qp0 decode’s luma plane IS the plane libx264
received\*\*, on every build, by definition. Referencing the metrics to it
removes the assumption instead of widening it, and it costs nothing extra —
`encode_ringing_excess` already needed that leg.

\*\*That “IS” is a claim about the leg’s INPUT FORMAT as much as its rate
control\*\*, and the leg pinned `-pix_fmt yuv420p` until an#72. Against a 4:4:4
delivery the reference was therefore a *different colour pipeline* from the file
it referenced, and every metric measured against it carried the whole 4:2:0
conversion this leg exists to cancel — as a term that does not cancel and does
vary by build.

The leg now takes its format from the **delivered file itself**
(`delivered_pix_fmt`, threaded in by `run.lossless_reference`). Probing rather
than re-deriving is the load-bearing part: **two** seams set the delivered
format — `RenderContext.pix_fmt`, which `an render --pix-fmt` uses and which the
mux resolves FIRST, and the `DEFAULT_PIX_FMT` module global, which is only its
fallback. A leg that consults either one covers only that one, and an#72’s first
fix consulted the global — so it silently re-pinned on the seam a user can
actually reach, with every guard green.

(No `pix_fmt` lever exists: `MUTATIONS` is `high_crf`/`disabled_aa`/`supersample`
and `_check_registered` enforces it at import. The global’s seam is kept for a
lever that has never been registered — see `an-dev-bench`’s `pix_fmt` row for why
— and outside the product it is driven only by tests. So “the lever rebinds it”
is a statement about a seam’s purpose, not about anything that runs.)

Note which metrics the mismatch reached: `flat_field_deviation`,
`flat_field_p99_dev` and `encode_flicker_on_held_pixels` reduce over RGB, so
chroma reaches them, while the luma-domain metrics never moved —
`coded_luma_edge_error` and `encode_ringing_excess` are identical to the last
digit between a 4:2:0 and a 4:4:4 leg on all ten corpus scenes.

Two things this does NOT change:

- **The chroma metric still references the direct RGB->444 conversion**, because
  its subject *is* the 4:2:0 subsampling that happens during the conversion —
  which is exactly the term this leg cancels, so a lossless-referenced version
  is blind to it whatever format the leg is encoded in. (With a tracking leg
  and a 4:4:4 delivery, both legs are 4:4:4 and the subsampling does not exist
  to be measured; tracking makes the panel honest, not sighted.) It does **not**
  read ~0, though, and that half of the old wording was wrong: measured
  2026-08-29 on four scenes, mean 

  ```
  |dCr|
  ```

   over the edge mask reads 1.71 / 1.90 /
  2.24 / 2.50 against the shipped metric’s 7.19 / 9.29 / 8.04 / 2.93, because it
  measures chroma *quantiser* damage instead (an#72).
- \*\*The PNG conversion is still performed and its distance from the encoder’s
  input is still recorded\*\* (`png_to_encoder_input_luma`), because that number
  is exactly the build-dependence that was hiding inside a hard equality. It is
  provenance now, not a gate.

**an#148 names what the build-dependence WAS, and both legs now pin it.** The
varying term was never this PNG leg — it was the *encoder’s* conversion:
`-colorspace bt709` sets the auto-inserted RGB->YUV matrix on ffmpeg 8/9 and
reaches only the VUI on ffmpeg 6.1, so on 6.1 libx264 received BT.601 planes
while the delivered file was tagged BT.709 (pure red at Y=81, against BT.709’s
62.6). `lossless_encode_command` therefore carries `-vf BT709_SCALE_FILTER`, the
same filter the delivered mux now passes: a lossless leg that converts
differently from the file it references is not “the plane libx264 received” on
any build where the two disagree, whatever its rate control. Keeping the two
spellings identical is why `SOURCE_SCALE_FILTER` is *bound* to
`an.base.BT709_SCALE_FILTER` rather than restating it.

`-map 0:v:0 -fps_mode passthrough` on every mp4 decode: the delivered file
carries an AAC track, and implicit frame-rate conversion would silently re-time
the sequence that every encode-side metric pairs frame-for-frame.

### Module Attributes

| [`SOURCE_SCALE_FILTER`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.SOURCE_SCALE_FILTER)   | without it the encode-side metrics measure a colour-space conversion.                                                                                                                                    |
|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`YUV_PIX_FMT`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.YUV_PIX_FMT)           | 4 for both legs of every encode-side metric — never `rgb24` for the edge metrics, whose defect was clipping precisely at the saturated fills sitting against black outlines that flat 2D art is made of. |

### Functions

| [`decoded_rgb`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.decoded_rgb)(mp4, \*, height, width)               | `(N, H, W, 3)` uint8 of the delivered mp4.                                      |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`decoded_rgb_command`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.decoded_rgb_command)(mp4)                          | Decode the delivered mp4 to raw RGB.                                            |
| [`decoded_yuv`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.decoded_yuv)(mp4, \*, height, width)               | `(N, 3, H, W)` uint8 planar YUV of the delivered mp4.                           |
| [`decoded_yuv_command`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.decoded_yuv_command)(mp4)                          | Decode the delivered mp4 to planar YUV.                                         |
| [`delivered_pix_fmt`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.delivered_pix_fmt)(mp4)                            | The pixel format `mp4` is actually encoded in, from the file itself.            |
| [`lossless_encode_command`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.lossless_encode_command)(frames_dir, fps, out, \*) | `-qp 0` with otherwise identical flags, for `encode_ringing_excess`.            |
| [`run_raw`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.run_raw)(cmd)                                      | Run an ffmpeg command and return its raw stdout, or raise with the stderr.      |
| [`source_rgb`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.source_rgb)(frames_dir, \*, height, width, frames) | `(N, H, W, 3)` uint8 of the pre-encode PNGs.                                    |
| [`source_rgb_command`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.source_rgb_command)(frames_dir)                    | Decode the pre-encode PNG sequence to raw RGB.                                  |
| [`source_yuv`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.source_yuv)(frames_dir, \*, height, width, frames) | `(N, 3, H, W)` uint8 planar YUV of the pre-encode PNGs, range-pinned.           |
| [`source_yuv_command`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.source_yuv_command)(frames_dir)                    | Decode the pre-encode PNG sequence to planar YUV, **range- and matrix-pinned**. |
| [`video_stream_bytes`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.video_stream_bytes)(mp4)                           | Sum of the video stream's packet sizes — the AAC track excluded.                |

### Exceptions

| [`BenchDecodeError`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.BenchDecodeError)   | ffmpeg could not read something the bench needs.   |
|---------------------------------------------------------------------|----------------------------------------------------|

### *exception* an.bench.imageio.BenchDecodeError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

ffmpeg could not read something the bench needs.

### an.bench.imageio.SOURCE_SCALE_FILTER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'scale=out_range=tv:out_color_matrix=bt709'*

without it
the encode-side metrics measure a colour-space conversion.

**Bound to the product’s own filter, not restated** (an#148). It used to be a
second copy of the same string, which was harmless only while the delivered
encode had no `-vf` of its own to disagree with. Now that it has one, two
copies is one edit away from the PNG leg and the encoder converting
differently — which is exactly the term the encode-side metrics cannot see
and would report as encoder damage.

* **Type:**
  The pinned conversion applied to the PNG leg. Never remove it

### an.bench.imageio.YUV_PIX_FMT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'yuv444p'*

4 for both legs of every encode-side metric — never `rgb24` for
the edge metrics, whose defect was clipping precisely at the saturated fills
sitting against black outlines that flat 2D art is made of.

* **Type:**
  Planar 4
* **Type:**
  4

### an.bench.imageio.decoded_rgb(mp4, , height, width)

`(N, H, W, 3)` uint8 of the delivered mp4.

`frames=None`: the run deliberately tolerates the encoder emitting a
different count from the source leg and records it as
`frame_count_disagreement`, so an equality here would turn a recorded
disagreement into a crash. See `_reshape()`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.bench.imageio.decoded_rgb_command(mp4)

Decode the delivered mp4 to raw RGB.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.bench.imageio.decoded_yuv(mp4, , height, width)

`(N, 3, H, W)` uint8 planar YUV of the delivered mp4. `frames=None` — see [`decoded_rgb()`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.decoded_rgb).

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.bench.imageio.decoded_yuv_command(mp4)

Decode the delivered mp4 to planar YUV.

No `scale` filter here, deliberately: the file carries BT.709 tags
(an#34) so ffmpeg already decodes it in the space the source leg is pinned
to. Adding one would convert twice.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.bench.imageio.delivered_pix_fmt(mp4)

The pixel format `mp4` is actually encoded in, from the file itself.

The delivered encode resolves its format from `RenderContext.pix_fmt`
**or** the module global (`render._check_pix_fmt`), and only the file knows
which won — so the lossless leg cannot re-derive it and be sure of matching.
Re-deriving is what an#72’s first fix did, and it covered the bench lever’s
seam (which rebinds the global) while missing the product’s own
(`an render --pix-fmt`, which sets the context and never touches it).

Probed, not remembered: this is the one source of truth that no future seam
can route around.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.bench.imageio.lossless_encode_command(frames_dir, fps, out, , pix_fmt=None)

`-qp 0` with otherwise identical flags, for `encode_ringing_excess`.

Identical to the delivered encode except for the rate control, so the
difference of the two overshoot means cancels the source-hardness term —
which is the whole reason `encode_ringing_excess` replaced raw overshoot.

**Pinned in its ENCODER SETTINGS, tracking in its INPUT FORMAT** — and the
asymmetry is the correction an#72 turned on. Two module globals reach this
command and they must be reached differently:

- `DETERMINISTIC_X264_ARGS` is bound at IMPORT, above, so a lever that
  rebinds it (`high_crf`) cannot move the reference. The reference has to
  stay lossless, or every encode-side metric is measured against a moving
  target and the lever produces beautiful numbers about nothing.
- `pix_fmt` is resolved at CALL time through the product’s own
  `_check_pix_fmt()`. The bench passes the
  > format **probed off the delivered mp4** ([`delivered_pix_fmt()`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.delivered_pix_fmt));
  > `None` falls back to `DEFAULT_PIX_FMT`, which is right for a caller
  > with no delivered file to match and wrong for one that has it.

The difference is not a preference. `-pix_fmt` is not an encoder setting:
it names **what libx264 receives**, and being what libx264 received is this
leg’s entire purpose (see the module docstring). Pinning it does not keep
the reference lossless — it makes the reference a \*different colour
pipeline\* from the delivered file, so every metric measured against it
silently acquires the whole 4:2:0 conversion the reference exists to
cancel. Measured on the corpus at 4:4:4: family E
(`encode_flicker_on_held_pixels`) changes SIGN on three of ten scenes
between a pinned leg and a tracking one — from as-declared to contrary in
every case — and family D (`flat_field_deviation`) moves by up to 24.8
percentage points. The affected metrics are exactly the RGB-domain ones,
which reduce over three channels so chroma reaches them; the luma-domain
metrics were never affected — `coded_luma_edge_error` and
`encode_ringing_excess` are identical to the last digit between a 4:2:0
and a 4:4:4 leg on all ten scenes.

A default (4:2:0) render is byte-identical to before this change, so no
committed ledger row is invalidated.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.bench.imageio.run_raw(cmd)

Run an ffmpeg command and return its raw stdout, or raise with the stderr.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### an.bench.imageio.source_rgb(frames_dir, , height, width, frames)

`(N, H, W, 3)` uint8 of the pre-encode PNGs.

`frames` is how many PNGs are on disk — the caller counted them — and it
is required rather than derived, so the decode cannot silently return a
different number of them.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.bench.imageio.source_rgb_command(frames_dir)

Decode the pre-encode PNG sequence to raw RGB.

RGB is correct here and only here: the two metrics that use it
(`flat_field_deviation`, `encode_flicker_on_held_pixels`) are masked to the
flat interior and to held pixels, both off-edge by construction, so the
clipping that ruins the edge metrics cannot reach them.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.bench.imageio.source_yuv(frames_dir, , height, width, frames)

`(N, 3, H, W)` uint8 planar YUV of the pre-encode PNGs, range-pinned.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.bench.imageio.source_yuv_command(frames_dir)

Decode the pre-encode PNG sequence to planar YUV, **range- and matrix-pinned**.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.bench.imageio.video_stream_bytes(mp4)

Sum of the video stream’s packet sizes — the AAC track excluded.

`file_bytes` includes the audio track, which the renderer always emits
(silent if there is no dialogue), so it varies with the audio cache’s state.
This one does not.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)


# _autosummary/an.bench.ledger.html.md

# an.bench.ledger

The ledger: three blocks that must never be mixed, and the guards that keep them apart.

`metrics` — numbers, each labelled **render-side** or **encode-side**. The
two families are blind to each other’s mutations by construction, and a
comparison that mixes them is comparing two different questions.

`tripwires` — change detectors. They fire on improvements and regressions
alike, so they count **zero** toward any criterion. A tripwire in the metrics
block is a boolean wearing a measurement’s clothes.

`provenance` — never gated, never counted. Everything needed to decide
whether two rows may be compared *at all*: the scene contract hash, the
resolved encode and decode commands, the mask parameters, the palette, and the
environment tuple split into a render side (**comparable on any machine** — the
pixels are ISA- and OS-invariant at a pinned Chromium build) and an encode side
(**machine-scoped** — a different x264 build moves the decoded stream by two
orders of magnitude, and a band wide enough to absorb that would swallow
`flat_field_deviation`’s entire crf18->23 signal).

Four value states, not two. `no change` and `null` are famously easy to
conflate, and conflating them lets any pre-encode statistic pad the witness
count for free:

### Module Attributes

| [`SCHEMA_VERSION`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.SCHEMA_VERSION)     | Bumped when a reader could misinterpret an older row.   |
|---------------------------------------------------------------------|---------------------------------------------------------|
| [`INLINE_SPEC_FIELDS`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.INLINE_SPEC_FIELDS) | Fields a per-scene row carries inline.                  |

### Functions

| [`build_ledger`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.build_ledger)(\*, provenance, scenes)            | The whole row.                                                          |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`build_scene_block`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.build_scene_block)(\*, provenance, metrics, ...) | Assemble one scene's three blocks, refusing anything unreadable.        |
| [`gated`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.gated)(gate[, detail])                           | Shorthand: this number would be uninterpretable, and here is the gate.  |
| [`measured`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.measured)(value, \*\*extra)                      | Shorthand: a real number.                                               |
| [`metric_declarations`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.metric_declarations)()                           | The full declaration of every metric and tripwire, once per ledger row. |
| [`unavailable`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.unavailable)(detail)                             | Shorthand: this check could not run, and here is why.                   |
| [`witnesses`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.witnesses)(ledger_scene, mutation)               | Which metrics would count for `mutation`, grouped by family.            |
| [`write_ledger`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.write_ledger)(ledger, path)                      | Write a row.                                                            |

### Classes

| [`Value`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.Value)(value[, state, gate, detail, extra])   | One measured (or deliberately absent) number.   |
|-----------------------------------------------------------------------------------------------|-------------------------------------------------|

### Exceptions

| [`LedgerSchemaError`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.LedgerSchemaError)   | A ledger row violates an invariant that would make it misreadable.   |
|----------------------------------------------------------------------|----------------------------------------------------------------------|

### an.bench.ledger.INLINE_SPEC_FIELDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('side', 'family', 'comparison_scope', 'reference', 'counts')*

Fields a per-scene row carries inline. Everything else about a metric — the
sentence, the notes, each prediction’s reason and reference — is identical
for every scene in a row and for every row, so it lives once in the
ledger-level `metric_declarations` block.

The split is not only size (it took a two-scene row from 54 KB to a third of
that, and an#38 quadruples the corpus). It is readability: a scene block you
can read is one where the numbers are not buried in the prose explaining
what the numbers are. What stays inline is exactly what
`an bench --compare` (an#40) keys on, so a comparison never has to consult
a second block to decide whether two rows may be compared at all.

### *exception* an.bench.ledger.LedgerSchemaError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A ledger row violates an invariant that would make it misreadable.

### an.bench.ledger.SCHEMA_VERSION *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 1*

Bumped when a reader could misinterpret an older row. `an bench --compare`
(an#40) must refuse a version it does not understand rather than guess.

### *class* an.bench.ledger.Value(value, state='measured', gate=None, detail='', extra=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One measured (or deliberately absent) number.

### an.bench.ledger.build_ledger(, provenance, scenes)

The whole row.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.ledger.build_scene_block(, provenance, metrics, tripwires)

Assemble one scene’s three blocks, refusing anything unreadable.

Completeness is enforced in both directions. A metric the registry declares
but the row omits is a silently narrower panel; a metric the row carries
but the registry does not declare has no family, no side and no predicted
direction, so nothing downstream can count it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.ledger.gated(gate, detail='')

Shorthand: this number would be uninterpretable, and here is the gate.

* **Return type:**
  [`Value`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.Value)

### an.bench.ledger.measured(value, \*\*extra)

Shorthand: a real number.

`nan` is refused rather than serialized: `json.dumps` emits the
non-standard literal `NaN`, which several strict readers reject, and a
metric whose mask was empty is an `unavailable`, not a number.

* **Return type:**
  [`Value`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.Value)

### an.bench.ledger.metric_declarations()

The full declaration of every metric and tripwire, once per ledger row.

Carried IN the row rather than referenced, because `--compare` reads rows
written by older registries: a row from six months ago has to be
interpretable without checking out the commit that wrote it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.ledger.unavailable(detail)

Shorthand: this check could not run, and here is why.

* **Return type:**
  [`Value`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.Value)

### an.bench.ledger.witnesses(ledger_scene, mutation)

Which metrics would count for `mutation`, grouped by family.

Reads the row rather than the registry, so an#41’s criterion is evaluated
against what was actually written down.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> witnesses({"metrics": {"m": {"family": "A",
...     "under_mutation": {"high_crf": {"counts": True}}}}}, "high_crf")
{'A': ['m']}
```

### an.bench.ledger.write_ledger(ledger, path)

Write a row. `sort_keys=True` so two rows diff line-for-line.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.bench.masks.html.md

# an.bench.masks

Every mask the panel is computed over, and the recorded operator for each.

Three of them serve the encode-side metrics, plus the ring. `EDGE_OPERATOR`
has a second spelling, `RENDER_EDGE_OPERATOR`: the same operator applied on
the **render** side, where the luma comes from the source RGB rather than from
ffmpeg. It is a second string rather than a reuse because it measures a
different plane — which is also what keeps family A’s mask off the encoder’s
toolchain, on the branch where ffmpeg is absent.

Each mask is derived **only from the reference (pre-encode) frames**, never
from the decoded ones. That is not a stylistic choice: a mask derived from the
decoded stream moves with the mutation, which is the defect that made
`edge_ssim` invisible on the AA arm (Δ = +0.0004, ~11x the machine band,
because the reference moved with the mutation).

Every operator here is exported as a **string** as well as a function, because
the ledger has to record what it measured — a threshold nobody can read back is
a threshold that quietly changes.

### Module Attributes

| [`EDGE_MASK_THRESHOLD`](_autosummary/an.bench.masks.html.md#an.bench.masks.EDGE_MASK_THRESHOLD)   | Two-pixel-apart luma gradient above this counts as an edge.                                                    |
|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| [`FLAT_DILATE_K`](_autosummary/an.bench.masks.html.md#an.bench.masks.FLAT_DILATE_K)         | Structuring element for the flat-field erosion.                                                                |
| [`RENDER_EDGE_OPERATOR`](_autosummary/an.bench.masks.html.md#an.bench.masks.RENDER_EDGE_OPERATOR)  | The SAME operator on the render side, and deliberately a second string rather than a reuse of `EDGE_OPERATOR`. |

### Functions

| [`dilate`](_autosummary/an.bench.masks.html.md#an.bench.masks.dilate)(mask[, k])                | Binary dilation by a `k x k` square, numpy-only.                        |
|-----------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`edge_mask`](_autosummary/an.bench.masks.html.md#an.bench.masks.edge_mask)(luma, \*[, threshold]) | Pixels straddling a hard luma step, from the reference plane only.      |
| [`flat_mask`](_autosummary/an.bench.masks.html.md#an.bench.masks.flat_mask)(rgb, \*[, k])          | The interior of large flat colour fields, from the source frames.       |
| [`held_mask`](_autosummary/an.bench.masks.html.md#an.bench.masks.held_mask)(rgb)                   | `(N-1, H, W)`: pixels the animator held perfectly still between frames. |
| [`ring_mask`](_autosummary/an.bench.masks.html.md#an.bench.masks.ring_mask)(edge)                  | The band immediately *around* an edge, excluding the edge itself.       |

### an.bench.masks.EDGE_MASK_THRESHOLD *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 40*

Two-pixel-apart luma gradient above this counts as an edge. Recorded in the
ledger with the operator string, because the research is explicit that the
prototype’s absolute numbers are ordinal evidence only and no threshold may
be written from them.

### an.bench.masks.FLAT_DILATE_K *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 3*

Structuring element for the flat-field erosion.

### an.bench.masks.RENDER_EDGE_OPERATOR *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'max(|Y[:,2:]-Y[:,:-2]|, |Y[2:,:]-Y[:-2,:]|) > 40, on FULL-RANGE BT.709 luma from the SOURCE RGB (an.bench.metrics.luma_u8)'*

The SAME operator on the render side, and deliberately a second string
rather than a reuse of `EDGE_OPERATOR`. `an.bench.imageio`’s
`SOURCE_SCALE_FILTER` pins `out_range=tv`, so ffmpeg’s Y lives in [16,235]
and its two-apart gradients are 219/255 of the full-range ones this operator
sees. At one threshold that makes this the wider mask ON A HARD SYNTHETIC
STEP — measured, a 45-code-value step selects 4 pixels here and 0 on the
limited-range plane — but NOT uniformly on real frames, where the two run
within a percent either way and the render-side one is the narrower on
`graded_field` and `single_character`. Which is the point: the two are
different measurements, and a reader can only know which one a row carries
if the row says which plane it measured.

### an.bench.masks.dilate(mask, k=3)

Binary dilation by a `k x k` square, numpy-only.

Implemented as shifted ORs rather than a convolution: `k` is 2 or 3 here,
so nine shifts beat pulling in scipy, and the package’s dependency
perimeter is four names wide on purpose.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> m = np.zeros((1, 5, 5), bool); m[0, 2, 2] = True
>>> int(dilate(m, 3)[0].sum())
9
```

### an.bench.masks.edge_mask(luma, , threshold=40)

Pixels straddling a hard luma step, from the reference plane only.

`luma` is `(N, H, W)` uint8. The border ring is excluded because a
two-apart difference is undefined there; excluding it is what keeps the
operator string honest.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> y = np.zeros((1, 5, 6), np.uint8); y[0, :, 3:] = 255
>>> int(edge_mask(y).sum())
6
```

### an.bench.masks.flat_mask(rgb, , k=3)

The interior of large flat colour fields, from the source frames.

~90% of a flat-cutout frame, and the part no edge metric touches. Banding
and blocking live here, and without this mask they are invisible.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> a = np.zeros((1, 9, 9, 3), np.uint8); a[0, :, 5:] = 255
>>> bool(flat_mask(a)[0, 0, 0]) and not bool(flat_mask(a)[0, 0, 5])
True
```

### an.bench.masks.held_mask(rgb)

`(N-1, H, W)`: pixels the animator held perfectly still between frames.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> a = np.zeros((2, 3, 3, 3), np.uint8); a[1, 0, 0] = 5
>>> int(held_mask(a).sum())
8
```

### an.bench.masks.ring_mask(edge)

The band immediately *around* an edge, excluding the edge itself.

Where ringing and mosquito noise land. Excluding the edge is what makes it
a different measurement from `coded_luma_edge_error` rather than a second
name for it.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> e = np.zeros((1, 5, 5), bool); e[0, 2, 2] = True
>>> int(ring_mask(e).sum())
8
```


# _autosummary/an.bench.metrics.html.md

# an.bench.metrics

The metric panel: pure numpy, no I/O, no subprocess.

Every function here is `f(arrays, mask) -> float`. Loading is
[`an.bench.imageio`](_autosummary/an.bench.imageio.html.md#module-an.bench.imageio)’s job. The split is what lets the whole panel run in
the default CI leg — numpy is a hard dependency, ffmpeg and Playwright are not
— which is the only part of this wave that main CI can ever see.

**The set is the research’s corrected one, and it is not the epic’s draft.**
All twelve originally-proposed metrics were refuted. The corrections that are
easiest to undo by accident, each guarded by a test:

- `edge_transition_width`’s flatness tolerance must not be 0. With `> 0`,
  ±3-LSB dither sends the metric to 255.0.
- `off_palette_pixel_fraction` must not use `np.unique(..., axis=0)`:
  1.833 s/frame at 1080p against 0.019 s for the packed form, 94x, identical
  result.
- `encode_flicker_on_held_pixels` must **cast before subtracting**.
  `np.abs(a - b)` on uint8 is the identity on unsigned dtypes, so the literal
  proposed form measured the *sign* of the change. It stayed monotone, which is
  exactly why it would have shipped unnoticed. And it must report a **rate**,
  not a mean: the median held-pixel delta is 0 at every CRF.
- `encode_ringing_excess` is the difference of two overshoot means, so the
  source-hardness term cancels. Raw overshoot is a joint function of source
  hardness and encoder fidelity with one degree of freedom, which is why any
  move toward crisper outlines raised it under an unchanged encoder.
- `edge_masked_distinct_colours` must be handed a mask built from
  [`luma_u8()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.luma_u8), never from [`luma709()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.luma709) directly. `luma709` returns
  > **float in [0,1]** and [`an.bench.masks.edge_mask()`](_autosummary/an.bench.masks.html.md#an.bench.masks.edge_mask) thresholds at \*\*40 on
  > 0-255\*\*, so the float form makes every two-apart gradient <= 1.0 and the mask
  > comes back **entirely empty** — measured: 0 selected pixels against 4 for the
  > same hard step. The metric would then be `nan` on every scene, which reads
  > as “the check could not run” rather than as a bug (an#55).

One metric the epic named is deliberately absent: `mean_adjacent_frame_ssim`
moves the **wrong way** (0.958 at crf18 -> 0.977 at crf51, because a crushed
video is smoother). Its existing use as a frozen-render detector in
[`an.verify.media_quality`](_autosummary/an.verify.media_quality.html.md#module-an.verify.media_quality) is a different and legitimate job.

### Module Attributes

| [`EDGE_FLAT_TOL`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.EDGE_FLAT_TOL)      | Two neighbours within this many code values count as "flat".                                                                                                     |
|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`EDGE_TRIM`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.EDGE_TRIM)          | Trim fraction for `edge_transition_width`'s mean, so one pathological run cannot carry the number.                                                               |
| [`FLAT_DEV_TOL`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.FLAT_DEV_TOL)       | A flat-field pixel more than this far off is "deviated".                                                                                                         |
| [`FLICKER_DELTA_TOL`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.FLICKER_DELTA_TOL)  | A held pixel that moved by at least this much "flickered".                                                                                                       |
| [`SSIM_RADIUS`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.SSIM_RADIUS)        | `ssim_map` window radius; 7x7, matched to feature size rather than the global-moment form.                                                                       |
| [`FLAT_DEV_TOL_SWEEP`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.FLAT_DEV_TOL_SWEEP) | The neighbourhood of each threshold counter's OWN free parameters over which its verdict must agree before `an bench-compare` will call it a direction (an#140). |
| [`BLEND_TOLERANCE`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.BLEND_TOLERANCE)    | How far off the line between two palette colours a pixel may sit and still be called a blend of them.                                                            |
| [`LUMA_709`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.LUMA_709)           | BT.709 luma coefficients, recorded in the ledger so a future change to the reduction is visible rather than folded into the number.                              |

### Functions

| [`classify_off_palette`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.classify_off_palette)(entries, palette)             | Say, for each off-palette colour, whether it is a blend of two declared ones.   |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`edge_masked_distinct_colours`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.edge_masked_distinct_colours)(packed, edge)         | Mean distinct colours per frame, counted ONLY on the edge mask.                 |
| [`edge_transition_width`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.edge_transition_width)(rgb, \*[, tol, trim])        | Average thickness, in pixels, of the fuzzy band between two flat areas.         |
| [`encode_flicker_on_held_pixels`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.encode_flicker_on_held_pixels)(src_rgb, ...)        | Fraction of perfectly-held pixels that moved in the delivered video.            |
| [`encode_flicker_sweep`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.encode_flicker_sweep)(src_rgb, dec_rgb, \*[, tols]) | `encode_flicker_on_held_pixels`' count at every `tol` of its sweep.             |
| [`encode_ringing_excess`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.encode_ringing_excess)(dec_luma, ...)               | How much more the lossy encode overshoots than a lossless one does.             |
| [`flat_field_deviation`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.flat_field_deviation)(src_rgb, dec_rgb, flat, \*)   | Fraction of flat-field pixels the encoder moved by more than `tol`.             |
| [`flat_field_deviation_sweep`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.flat_field_deviation_sweep)(src_rgb, dec_rgb, ...)  | `flat_field_deviation`'s count at every `(dilate_k, tol)` of its sweep.         |
| [`frame_distinct_colours`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.frame_distinct_colours)(packed)                     | Mean number of distinct colours per frame.                                      |
| [`golden_comparison`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.golden_comparison)(today_rgb, golden_rgb)           | The full-frame identity gate plus its diagnostics.                              |
| [`luma709`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.luma709)(rgb)                                       | `(H,W,3)` uint8 -> `(H,W)` float in [0,1].                                      |
| [`luma_u8`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.luma_u8)(rgb)                                       | `(...,3)` uint8 -> `(...)` uint8 luma, on the 0-255 scale a plane uses.         |
| [`masked_mean_abs`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.masked_mean_abs)(a, b, mask)                        | `|a - b|` averaged over `mask`, with the cast that makes it correct.            |
| [`off_palette_pixel_fraction`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.off_palette_pixel_fraction)(packed, palette)        | Fraction of the frame whose colour is not one the compiler declared.            |
| [`off_palette_top_colours`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.off_palette_top_colours)(packed, palette, \*)       | The most frequent off-palette colours, as `[{"hex", "count"}]`.                 |
| [`overshoot_mean`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.overshoot_mean)(dec_luma, src_luma, ring)           | Mean positive excursion above the source, over the ring band.                   |
| [`pack_rgb`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.pack_rgb)(rgb)                                      | `(N,H,W,3)` uint8 -> `(N,H,W)` uint32, one integer per colour.                  |
| [`ssim_map`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.ssim_map)(a, b, \*[, r])                            | Windowed SSIM at stride 1, as a per-pixel map.                                  |
| [`sweep_cell_key`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.sweep_cell_key)(\*\*params)                         | The label of one cell of a parameter sweep, stable across rows.                 |

### an.bench.metrics.BLEND_TOLERANCE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 3*

How far off the line between two palette colours a pixel may sit and still
be called a blend of them. 8-bit channels, so a couple of code values covers
rounding in the compositor.

### an.bench.metrics.EDGE_FLAT_TOL *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 4*

Two neighbours within this many code values count as “flat”. NOT 0 — see
the module docstring.

### an.bench.metrics.EDGE_TRIM *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.1*

Trim fraction for `edge_transition_width`’s mean, so one pathological run
cannot carry the number.

### an.bench.metrics.FLAT_DEV_TOL *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 6*

A flat-field pixel more than this far off is “deviated”.

### an.bench.metrics.FLAT_DEV_TOL_SWEEP *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), ...]* *= (4, 5, 6, 7, 8)*

The neighbourhood of each threshold counter’s OWN free parameters over which
its verdict must agree before `an bench-compare` will call it a direction
(an#140). `flat_field_deviation` is `(dev > tol).mean()` over a mask
eroded by `FLAT_DILATE_K`; both are free, and measured on one machine and
one x264 build its direction under a 4:2:0 -> 4:4:4 change reversed on three
of six scenes somewhere on this grid (`graded_field` +84.2% at tol 6,
-81.6% at tol 8). Every shipped value sits inside its own sweep, so a sweep
that disagrees is the row saying “somewhere near my own settings, my
direction reverses”. The grids are the ones the defect was measured on, not
tuned ones; the flicker grid is one-sided below because tol 0 counts every
pixel.

### an.bench.metrics.FLICKER_DELTA_TOL *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 2*

A held pixel that moved by at least this much “flickered”.

### an.bench.metrics.LUMA_709 *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= (0.2126, 0.7152, 0.0722)*

BT.709 luma coefficients, recorded in the ledger so a future change to the
reduction is visible rather than folded into the number.

### an.bench.metrics.SSIM_RADIUS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 3*

`ssim_map` window radius; 7x7, matched to feature size rather than the
global-moment form.

### an.bench.metrics.classify_off_palette(entries, palette)

Say, for each off-palette colour, whether it is a blend of two declared ones.

This is the difference between a diagnostic and a list of hex codes. The
metric’s whole meaning is “not one of the colours the compiler declared”,
so a palette that under-collects turns it into a large, plausible number
with no error anywhere. Anti-aliasing legitimately produces colours \*on the
segment between\* two declared colours; a missed literal does not.

So each entry gains `blend_of` (the two palette colours it sits between,
or `None`). A row whose top off-palette colours are all blends is
reporting anti-aliasing correctly. One that is not is a derivation bug, and
now it says so in the ledger rather than waiting for someone to look.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

```pycon
>>> classify_off_palette([{"hex": "#808080"}], [0x000000, 0xffffff])[0]["blend_of"]
['#000000', '#ffffff']
>>> classify_off_palette([{"hex": "#ff00ff"}], [0x000000, 0xffffff])[0]["blend_of"]
```

### an.bench.metrics.edge_masked_distinct_colours(packed, edge)

Mean distinct colours per frame, counted ONLY on the edge mask.

The half of `frame_distinct_colours` that is about edges: an interior-only
change — a gradient laid into a flat field, a soft shadow — moves the
whole-frame count and cannot reach this one at all. The second doctest
below is that property, and it is the whole of what the mask buys.

\*\*It does NOT make the number blind to a whole-frame blur, and an#55’s
premise that it would is refuted.\*\* The mask is recomputed from the frame
being measured, and a blur WIDENS the edge band, so the mask grows to admit
the new gradation. Measured on the six committed goldens, 3x3 box blur,
ratio against k=1:

| scene             | whole-frame   | edge-masked   |
|-------------------|---------------|---------------|
| aa_probe          | 10.25x        | 9.50x         |
| graded_field      | 2.04x         | 2.35x         |
| multi_shot        | 7.92x         | 4.63x         |
| promote_demo      | 1.25x         | 0.83x         |
| saturated_outline | 1.49x         | 1.14x         |
| single_character  | 8.60x         | 5.70x         |

Damped on four of six, WORSE on `graded_field`, and nowhere near blind.
(an#55 quotes “1.8x-9.3x” for the whole-frame column; the real range on
these goldens is 1.25x-10.25x, wider at both ends.)

\*\*What separates a blur from a supersample is the WIDTH half of the pair,
not this one.\*\* The same blur puts `edge_transition_width` at 2.1x-3.4x
(`aa_probe` 2.655 -> 5.594 px), while an exact k=2 resolve moves it +2.6%
to +8.0% (research §4). So read this metric BESIDE
`edge_transition_width`: colours up with width flat is gradation added;
colours up with width doubled is a soft picture.

**Evidence for a human reader, never a gate.** an#41’s criterion counts
metrics independently and cannot express a conjunction, so neither half of
the pair may be declared as counting on the strength of the other.

`edge` must come from [`an.bench.masks.edge_mask()`](_autosummary/an.bench.masks.html.md#an.bench.masks.edge_mask) applied to
[`luma_u8()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.luma_u8) — **not** to [`luma709()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.luma709), which is float in [0,1]
against a threshold of 40 on 0-255 and yields an empty mask every time.

Returns `(mean, frames_measured)`. A frame whose mask is empty is
**skipped, not averaged in as zero** — a zero would drag the mean down and
read as exactly the regression this metric exists to notice. With no such
frame the answer is `nan`, which the caller records as `unavailable`;
[`an.bench.ledger.measured()`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.measured) refuses it.

```pycon
>>> import numpy as np
>>> from an.bench.masks import edge_mask
>>> c = np.zeros((1, 4, 16, 3), np.uint8); c[0, :, 8:] = 255
>>> edge_masked_distinct_colours(pack_rgb(c), edge_mask(luma_u8(c)))
(2.0, 1)
```

A change entirely inside a flat field moves the whole-frame count and not
this one — this, and only this, is what the mask buys:

```pycon
>>> d = c.copy()
>>> for x in range(2, 6): d[0, :, x] = 8 * (x - 1)
...
>>> frame_distinct_colours(pack_rgb(d))
6.0
>>> edge_masked_distinct_colours(pack_rgb(d), edge_mask(luma_u8(d)))
(2.0, 1)
```

An empty mask is not a colour count of zero:

```pycon
>>> flat = np.full((1, 4, 8, 3), 128, np.uint8)
>>> edge_masked_distinct_colours(pack_rgb(flat), edge_mask(luma_u8(flat)))
(nan, 0)
```

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### an.bench.metrics.edge_transition_width(rgb, , tol=4, trim=0.1)

Average thickness, in pixels, of the fuzzy band between two flat areas.

Under 1 is a jagged staircase; ~1 is clean AA; 3+ means the picture has
gone soft. **Two-sided**, which is the whole reason it replaced a palette
cardinality count: cardinality reads AA-off as 4, AA-on as 45 and a 3x3
blur as 416, so “the number went up” means “AA restored” and “the picture
went soft” indiscriminately.

Returns `(trimmed_mean, median)`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> import numpy as np
>>> a = np.zeros((1, 4, 8, 3), np.uint8); a[0, :, 4:] = 255
>>> round(edge_transition_width(a)[0], 3)   # one hard step = a 2px band
2.0
```

### an.bench.metrics.encode_flicker_on_held_pixels(src_rgb, dec_rgb, , tol=2)

Fraction of perfectly-held pixels that moved in the delivered video.

Held-pose “boiling” — the worst artefact for limited-motion animation.
Pooled over every frame pair rather than averaged per pair, so a pair with
three held pixels does not weigh as much as one with seventy thousand.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> import numpy as np
>>> s = np.zeros((2, 1, 4, 3), np.uint8)
>>> d = s.copy(); d[1, 0, 0] = 5
>>> encode_flicker_on_held_pixels(s, d)
0.25
```

### an.bench.metrics.encode_flicker_sweep(src_rgb, dec_rgb, , tols=(1, 2, 3, 4))

`encode_flicker_on_held_pixels`’ count at every `tol` of its sweep.

Same shape and same purpose as [`flat_field_deviation_sweep()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.flat_field_deviation_sweep): this is
the panel’s other hard-threshold counter, and measured under the same
4:2:0 -> 4:4:4 change its direction reverses between tol 2 and tol 3 on
`single_character` (+25% -> -15%), and under `high_crf` between tol 1
and tol 2 on `graded_field` (-9% -> +29%). Empty when nothing is held.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]]

```pycon
>>> import numpy as np
>>> s = np.zeros((2, 1, 4, 3), np.uint8)
>>> d = s.copy(); d[1, 0, 0] = 2
>>> encode_flicker_sweep(s, d, tols=(2, 3))
{'tol=2': [1, 4], 'tol=3': [0, 4]}
```

### an.bench.metrics.encode_ringing_excess(dec_luma, lossless_luma, src_luma, ring)

How much more the lossy encode overshoots than a lossless one does.

Both legs share the source, so the source-spectrum term cancels: AA-off
raises both together (correctly reporting “the encoder did not get worse”),
a genuine crispness improvement also raises both (no false regression), and
a CRF change raises only the lossy leg.

**Provisional.** `edge_band_mae` is recorded beside it so research open
question 4 — “does plain edge-band MAE beat this?” — is answered by the
ledger rather than by nobody.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> import numpy as np
>>> s = np.zeros((1, 1, 2), np.uint8)
>>> encode_ringing_excess(np.array([[[9, 0]]], np.uint8),
...                       np.array([[[3, 0]]], np.uint8), s, np.ones((1, 1, 2), bool))
3.0
```

### an.bench.metrics.flat_field_deviation(src_rgb, dec_rgb, flat, , tol=6)

Fraction of flat-field pixels the encoder moved by more than `tol`.

The strongest metric in the set and the only one measured genuinely
orthogonal to the edge/AA axis: monotone over a 133x span on the CRF ladder
(0.0003 -> 0.0399, crf18 -> crf51) and flat under every AA variant.

Returns `(fraction_over_tol, p99_of_the_deviation)`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> import numpy as np
>>> s = np.zeros((1, 3, 3, 3), np.uint8)
>>> d = s.copy(); d[0, 1, 1] = 20
>>> flat_field_deviation(s, d, np.ones((1, 3, 3), bool))[0]
0.1111111111111111
```

### an.bench.metrics.flat_field_deviation_sweep(src_rgb, dec_rgb, , mask_rgb, tols=(4, 5, 6, 7, 8), ks=(1, 3, 5))

`flat_field_deviation`’s count at every `(dilate_k, tol)` of its sweep.

`{cell_key: [counted, of]}` — integers, so a comparison between two rows
is exact rather than a comparison of two rounded fractions. The positional
pair is `flat_field_deviation`’s own (reference, decoded); `mask_rgb`
— what the flat mask is derived from, the SOURCE frames in `run.py` — is
keyword-only so the two roles cannot be swapped positionally. A `k`
whose mask selects nothing is omitted, not recorded as `[0, 0]`.

It is not a new number for the panel. It is what lets `an bench-compare`
tell a verdict from a threshold accident (an#140): the shipped cell IS the
metric, and the others say whether anywhere else on the declared grid of
its own two parameters the direction reverses.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]]

```pycon
>>> import numpy as np
>>> s = np.zeros((1, 9, 9, 3), np.uint8)
>>> d = s.copy(); d[0, 4, 4] = 7
>>> cells = flat_field_deviation_sweep(s, d, mask_rgb=s, tols=(6, 7), ks=(3,))
>>> cells
{'dilate_k=3,tol=6': [1, 81], 'dilate_k=3,tol=7': [0, 81]}
```

### an.bench.metrics.frame_distinct_colours(packed)

Mean number of distinct colours per frame.

A flatness / palette-discipline **guard**, with no predicted direction on
an AA change. Do not count it alongside `off_palette_pixel_fraction`; they
are the same family.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> import numpy as np
>>> frame_distinct_colours(np.array([[[1, 1, 2]], [[3, 4, 5]]], np.uint32))
2.5
```

### an.bench.metrics.golden_comparison(today_rgb, golden_rgb)

The full-frame identity gate plus its diagnostics.

Full-frame, not edge-masked: that is what makes the one sentence true, and
what catches the flat-interior regressions an edge mask is blind to — and
for this look the flat fields are most of the picture.

`changed_px` and `max_delta` belong in a failure message, not in the
metrics block; they are returned here so the caller can put them there.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> import numpy as np
>>> a = np.zeros((4, 4, 3), np.uint8)
>>> golden_comparison(a, a)["identical"]
True
```

### an.bench.metrics.luma709(rgb)

`(H,W,3)` uint8 -> `(H,W)` float in [0,1]. PIL-free.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> round(float(luma709(np.full((1, 1, 3), 255, np.uint8))[0, 0]), 6)
1.0
```

### an.bench.metrics.luma_u8(rgb)

`(...,3)` uint8 -> `(...)` uint8 luma, on the 0-255 scale a plane uses.

The one conversion between [`luma709()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.luma709), which returns \*\*float in
[0,1]\*\*, and [`an.bench.masks.edge_mask()`](_autosummary/an.bench.masks.html.md#an.bench.masks.edge_mask), whose threshold is \*\*40 on
0-255\*\*. Handing the float straight to the mask is not a wrong number, it is
an **empty mask**: every two-apart gradient is <= 1.0, so nothing is ever an
edge and the metric downstream reads `unavailable` on every scene. Written
once, named and tested here rather than open-coded at each call site,
because it cost a debugging round the first time (an#55).

**This is FULL-RANGE luma and the encode-side plane is not.**
[`an.bench.imageio.SOURCE_SCALE_FILTER`](_autosummary/an.bench.imageio.html.md#an.bench.imageio.SOURCE_SCALE_FILTER) pins `out_range=tv`, so
ffmpeg’s Y sits in [16,235] and its gradients are 219/255 of these. At one
threshold that makes the render-side mask the **wider** of the two —
measured: a 45-code-value step selects 4 pixels here and 0 there — which is
why the row records it under its own operator string rather than reusing
`an.bench.masks.EDGE_OPERATOR`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> a = np.zeros((1, 1, 2, 3), np.uint8); a[0, 0, 1] = 255
>>> luma_u8(a).tolist()
[[[0, 255]]]
```

### an.bench.metrics.masked_mean_abs(a, b, mask)

`|a - b|` averaged over `mask`, with the cast that makes it correct.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> import numpy as np
>>> x = np.array([[[10, 200]]], np.uint8); y = np.array([[[12, 190]]], np.uint8)
>>> masked_mean_abs(x, y, np.ones((1, 1, 2), bool))
6.0
```

### an.bench.metrics.off_palette_pixel_fraction(packed, palette)

Fraction of the frame whose colour is not one the compiler declared.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> import numpy as np
>>> p = np.array([[[0x000000, 0xffffff, 0x808080]]], np.uint32)
>>> round(off_palette_pixel_fraction(p, [0x000000, 0xffffff]), 4)
0.3333
```

### an.bench.metrics.off_palette_top_colours(packed, palette, , top=10)

The most frequent off-palette colours, as `[{"hex", "count"}]`.

Not decoration: this metric’s whole meaning is “not one of the colours the
compiler declared”, so a palette that under-collects turns it into a large
plausible number with no error anywhere. If the top entries are exact hex
literals from the staged art, the derivation missed them; if they are
blends sitting between two palette entries, that is anti-aliasing and the
number is right.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

```pycon
>>> import numpy as np
>>> p = np.array([[[1, 1, 2]]], np.uint32)
>>> off_palette_top_colours(p, [2], top=1)
[{'hex': '#000001', 'count': 2}]
```

### an.bench.metrics.overshoot_mean(dec_luma, src_luma, ring)

Mean positive excursion above the source, over the ring band.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> import numpy as np
>>> s = np.zeros((1, 1, 2), np.uint8); d = np.array([[[5, 0]]], np.uint8)
>>> overshoot_mean(d, s, np.ones((1, 1, 2), bool))
2.5
```

### an.bench.metrics.pack_rgb(rgb)

`(N,H,W,3)` uint8 -> `(N,H,W)` uint32, one integer per colour.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> hex(int(pack_rgb(np.array([[[[0x12, 0x34, 0x56]]]], np.uint8))[0, 0, 0]))
'0x123456'
```

### an.bench.metrics.ssim_map(a, b, , r=3)

Windowed SSIM at stride 1, as a per-pixel map.

Added **beside** [`an.verify.media.ssim()`](_autosummary/an.verify.media.html.md#an.verify.media.ssim), never replacing it: that
function’s global-moment form is what
[`an.verify.media_quality`](_autosummary/an.verify.media_quality.html.md#module-an.verify.media_quality)’s frozen-render threshold was tuned
against, and `MediaQualityVerifier` sits in the default orchestrate chain.

Why it exists at all: the metrics survey concluded SSIM should be excluded
because whole-frame SSIM scores a total eye-blink at 0.9989. That
conclusion was **refuted** — only the *global-moment* reduction is blind.
With the window matched to feature size, min-over-windows scores the same
blink at 0.279 (1080p) and 0.063 (native).

Do not cross-check against ffmpeg’s `ssim` filter: it uses overlapped 8x8
block sums at 4-pixel stride, disagrees by up to 0.0201, and the
disagreement *grows* with degradation.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> x = np.linspace(0, 1, 64).reshape(8, 8)
>>> round(float(ssim_map(x, x).min()), 6)
1.0
```

### an.bench.metrics.sweep_cell_key(\*\*params)

The label of one cell of a parameter sweep, stable across rows.

Keyword order is irrelevant — the key is sorted — so two rows written by
code that spells the call differently still match cell for cell.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> sweep_cell_key(tol=6, dilate_k=3)
'dilate_k=3,tol=6'
```


# _autosummary/an.bench.mutants.html.md

# an.bench.mutants

The guard mutants: “I mutation-tested it” as a runnable artifact, not a claim.

Wave 1 shipped three guards that stayed green when the bug they guarded was
reintroduced — one flagged the sentence *explaining* a deprecation, one
exonerated everything via neighbouring prose, and one keyed on a string the fix
itself removed. Wave 2’s own an#36 sweep lost five mutants, two of them to
guards that asserted a table’s *contents* rather than the check that reads it.

Every commit in this wave has said “N mutants, all caught”. That sentence is
unfalsifiable after the fact: the mutations lived in a scratch script and were
thrown away. This module is the correction — each one is **declared data**, so
a future reader can re-run the proof instead of trusting the commit message.

Three properties the declaration is shaped to have:

**Each mutant names the guard it must break.** A mutation nobody expected to be
caught is a fact about the code; a mutation with a named catcher is a claim
about a *test*, and that is what is worth pinning.

\*\*The whole file runs, never a `-k` filter.\*\* A filter that happens to exclude
the catching test reports “not caught” and sends the reader to write a test
that already exists.

\*\*The `old` text is pinned exactly.\*\* A mutant whose source text has moved is
a mutant that silently stops proving anything, so
`tests/test_bench_mutation.py` asserts every site still exists — cheaply, in
the default CI leg, with no pytest subprocesses at all. The full sweep is
`an bench-mutants`: it is a deliberate act, and it takes about half a minute.

**A killed sweep must not leave the tree mutated** (an#67), and that is two
mechanisms rather than one, because they cover different kills. SIGTERM is
turned into an exception for the duration ([`restore_on_termination()`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.restore_on_termination)) so
the restoring `finally` runs; SIGKILL cannot be caught by anything, so
[`check_sites()`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.check_sites) — which every sweep runs first, and which the default CI leg
runs too — recognises a file whose mutated text is present and whose original is
gone, and reports it as an interrupted run with the exact repair. The recovery
path is the load-bearing half: what makes a leftover dangerous is that every
mutation here is chosen to be *plausible*, so the tree compiles, renders and
stays green apart from one test, and a developer can commit it without noticing.
That recovery reads “the mutation is present and the original is gone”, which is
an assumption about the DECLARATION — so `check_sites` also refuses a mutant
whose substitution leaves its own `old` text behind, because such a mutant is
invisible to the recovery and a later sweep would restore *to* the leftover.

### Module Attributes

| [`PYTEST_ARGS`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.PYTEST_ARGS)           | see the module docstring.                                                                                                                                                                                                                                                                                                                                     |
|------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`RESTORE_ON_SIGNALS`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.RESTORE_ON_SIGNALS)    | Terminating signals turned into an exception for the duration of a sweep, so the restoring `finally` runs.                                                                                                                                                                                                                                                    |
| [`INTERRUPTED_EXIT_CODE`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.INTERRUPTED_EXIT_CODE) | the shell convention of 128 + SIGINT.                                                                                                                                                                                                                                                                                                                         |
| [`SWEEP_COPY_IGNORE`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.SWEEP_COPY_IGNORE)     | Left out of a sweep's throwaway tree.                                                                                                                                                                                                                                                                                                                         |
| [`MUTANTS`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.MUTANTS)               | A representative sweep rather than an exhaustive one, chosen so each entry pins a *different* class of failure: a silently widened comparison, a guard that reads the same table twice, a refusal that stops refusing, an unknown-is-not-zero substitution, a decoder that only works on its own output, and an instrument that goes blind without saying so. |
| [`RESTORE_TMP_SUFFIX`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.RESTORE_TMP_SUFFIX)    | Suffix for the sibling file the restore is staged through.                                                                                                                                                                                                                                                                                                    |

### Functions

| [`check_sites`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.check_sites)([root])               | Problems with the declarations themselves, as a list of sentences.           |
|------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`format_results`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.format_results)(results)           | The digest, with the survivors and errors last because they are the finding. |
| [`restore_on_termination`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.restore_on_termination)([signals]) | Turn a terminating signal into an exception for the duration.                |
| [`run_mutants`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.run_mutants)([names, root])        | Apply each mutant, run its whole guard file, restore, and report.            |
| [`sweep_tree`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.sweep_tree)(source)                | A throwaway copy of `source` for a sweep to do its damage in.                |
| [`verdict_of`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.verdict_of)(result)                | `CAUGHT` / `SURVIVED` / `ERRORED`, as three separate answers.                |

### Classes

| [`Mutant`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.Mutant)(name, file, old, new, caught_by, why)   | One deliberate defect, and the guard that must notice it.   |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------|

### Exceptions

| [`MutantError`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.MutantError)          | A declared mutant no longer applies, or the tree was left dirty.       |
|-----------------------------------------------------------------------|------------------------------------------------------------------------|
| [`MutantRunInterrupted`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.MutantRunInterrupted) | A terminating signal arrived mid-sweep, raised so the restore can run. |

### an.bench.mutants.INTERRUPTED_EXIT_CODE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 130*

the shell convention of
128 + SIGINT. Nonzero, and distinguishable from the 1 a surviving mutant
gives — “you stopped it” and “a guard is decoration” are different answers.

* **Type:**
  What the CLI exits with after an interrupted sweep

### an.bench.mutants.MUTANTS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Mutant](_autosummary/an.bench.mutants.html.md#an.bench.mutants.Mutant), ...]* *= (Mutant(name='png_paeth_tiebreak', file='an/bench/png.py', old='if (pa <= pb and pa <= pc) else (b if pb <= pc else c)', new='if (pa <= pb and pa <= pc) else (b if pb < pc else c)', caught_by='tests/test_bench_png.py', why="the Paeth predictor's tie-break. Wrong, it still decodes this module's own filter-0 output perfectly and corrupts every real Chromium frame — the exact asymmetry that makes an encoder validating its own decoder worthless."), Mutant(name='png_first_idat_only', file='an/bench/png.py', old='            idat.append(payload)', new='            idat = [payload]', caught_by='tests/test_bench_png.py', why='Chromium splits the stream: a real frame has 2-9 IDAT chunks and our own output has one, so a first-chunk-only reader passes its own tests and fails on everything else.'), Mutant(name='png_no_write_verification', file='an/bench/png.py', old='    if not np.array_equal(decode_png(out.read_bytes()), np.asarray(rgb)):', new='    if False:', caught_by='tests/test_bench_png.py', why="the only thing between a bug in this module's own encoder and a committed golden that silently disagrees with the frame it was blessed from."), Mutant(name='golden_criterion_becomes_file_bytes', file='an/bench/golden.py', old='    digest.update(f"{arr.dtype.str}:{arr.shape}|".encode("ascii"))', new='    pass', caught_by='tests/test_bench_golden.py', why='\`ndarray.tobytes()\` carries no shape, so a transposed frame hashes identically and satisfies the criterion an#38 literally states.'), Mutant(name='golden_blesses_a_blank_reason', file='an/bench/golden.py', old='    if not reason.strip():', new='    if reason is None:', caught_by='tests/test_bench_golden.py', why='a re-bless with no recorded reason is the same failure as a silently widened threshold — the named failure mode this wave exists to end.'), Mutant(name='golden_blesses_an_identical_pair', file='an/bench/golden.py', old='            if np.array_equal(decoded[i], decoded[j]):', new='            if False:', caught_by='tests/test_bench_golden.py', why='measured on \`promote_demo\`: frame 0 and the duration/2 frame differ by ZERO pixels, so the obvious second time blesses one picture twice and the second golden tests nothing forever after.'), Mutant(name='compare_gains_a_tolerance_band', file='an/bench/compare.py', old='    if before == after:\\n        return "no_change"', new='    if abs(float(before) - float(after)) < 1e-9:\\n        return "no_change"', caught_by='tests/test_bench_compare.py', why='two consecutive runs on one machine are bit-identical, so a band can only ever hide a true movement.'), Mutant(name='compare_refuses_on_an_absent_key', file='an/bench/compare.py', old='        elif b is \_ABSENT or a is \_ABSENT:', new='        elif False:', caught_by='tests/test_bench_compare.py', why='the ledger grows additively, so treating absence as difference makes every future field retroactively destroy comparability with every row already written.'), Mutant(name='compare_counts_metrics_not_families', file='an/bench/compare.py', old='        block["family_count"] = len(families)', new='        block["family_count"] = sum(len(v) for v in families.values())', caught_by='tests/test_bench_compare.py', why="counting bare metrics is satisfiable by shipping one signal under three names, which is exactly what family A's three edge metrics would do."), Mutant(name='compare_exempts_the_whole_environment', file='an/bench/compare.py', old='        touched = {t.label for t in MUTATION_TOUCHES.get(mutation, ())}', new='        touched = {i["key"] for i in common + render + encode}', caught_by='tests/test_bench_compare.py', why='the knob the lever pulls is the independent variable; the ISA is not. A blanket exemption lets a row from another machine in through the same door.'), Mutant(name='compare_exempts_by_path_not_by_value', file='an/bench/registry.py', old='        if self.differs_only_in is None:\\n            return True', new='        if True:\\n            return True', caught_by='tests/test_bench_compare.py', why="\`x264_argv\` is the WHOLE encode command, so exempting the path exempts every flag in it. A \`-preset medium\` -> \`-preset veryslow\` change moves every encode-side number and rode in as 'the lever moved it — expected'. The exemption must match the change the lever actually makes."), Mutant(name='compare_trusts_an_edited_prediction', file='an/bench/compare.py', old='    if not isinstance(inline, dict) or not isinstance(declared, dict):\\n        return []', new='    if True:\\n        return []', caught_by='tests/test_bench_compare.py', why="the prediction IS the criterion, and it is read from the after row's inline block alone. Flipping one \`expect\` turns \`contrary\` into \`as_declared\` with nothing else in the report moving — the cheapest possible way to fake a caught mutation."), Mutant(name='compare_lets_a_row_forge_its_own_scope', file='an/bench/compare.py', old='    "comparison_scope",\\n    "reference",', new='    "reference",', caught_by='tests/test_bench_compare.py', why="\`comparison_scope\` decides whether a metric may be compared ACROSS MACHINES, and \`compare\` reads the row's INLINE copy. Editing that one word compared an encode-side metric across a different ISA with no refusal — the single invariant this module exists to hold, defeated from inside the row."), Mutant(name='ledger_substitutes_zero_for_unknown', file='an/bench/ledger.py', old='        if self.state == "measured":\\n            if self.value is None:', new='        if self.state == "measured":\\n            if False:', caught_by='tests/test_bench_ledger_schema.py', why='a substituted number — 0.0 especially — is read downstream as a measurement, which is the unknown-is-not-zero failure the whole schema exists to prevent.'), Mutant(name='ledger_lets_a_tripwire_vanish', file='an/bench/ledger.py', old='    absent_tw = sorted(set(TRIPWIRES) - set(tripwires))', new='    absent_tw = []', caught_by='tests/test_bench_ledger_schema.py', why='a change detector that quietly stopped being computed reads exactly like one that fired and found nothing.'), Mutant(name='registry_counts_a_tautology', file='an/bench/registry.py', old='        if self.expect in ("no_change", "not_applicable") and self.counts:', new='        if False:', caught_by='tests/test_bench_ledger_schema.py', why="'no change by construction' is a tautology; counting it lets any pre-encode statistic pad the witness count for free."), Mutant(name='golden_fabricates_a_zero_pixel_count', file='an/bench/golden.py', old='"changed_px": max((int(f["changed_px"]) for f in compared), default=None),', new='"changed_px": max((int(f["changed_px"] or 0) for f in frames), default=0),', caught_by='tests/test_bench_golden.py', why="a shape mismatch has no per-pixel comparison to count, and turning that into 0 printed 'GOLDEN MISMATCH: 0 px changed' — a fabricated number in the one schema whose whole premise is that unknown is not zero."), Mutant(name='compare_scope_absence_fails_open', file='an/bench/compare.py', old='        if scope not in env_refusals:', new='        if False:', caught_by='tests/test_bench_compare.py', why="an absent \`comparison_scope\` read as 'no refusals apply', so an encode-side metric from another ISA and another x264 build compared cleanly and reported a regression."), Mutant(name='strict_passes_a_comparison_that_compared_nothing', file='an/tools.py', old='            not report.get("answered")', new='            False', caught_by='tests/test_bench_compare.py', why="the documented CI gate exited 0 on a run in which every scene was refused, while printing '0 regression(s)' — a zero the compare module's own docstring calls worse than no number at all."), Mutant(name='cli_returns_nothing_to_the_terminal', file='an/_\_main_\_.py', old='        if result is not None:\\n            typer.echo(result)', new='        pass', caught_by='tests/test_cli_dispatch.py', why='typer discards return values and every \`an.tools\` function returns its report as a string, so the CLI would run correctly and print NOTHING — the worst possible failure for a diagnostic tool.'), Mutant(name='cli_loses_the_signature_that_is_the_command_line', file='an/_\_main_\_.py', old='    @functools.wraps(func)\\n    def run(', new='    def run(', caught_by='tests/test_cli_dispatch.py', why='\`inspect.signature\` follows \`_\_wrapped_\_\`, and that signature IS the command line. Without it typer sees \`(\*args, \*\*kwargs)\` and every flag on all 17 commands disappears at once, while \`--help\` still renders.'), Mutant(name='corpus_reads_shot_order_from_the_directory', file='an/bench/corpus.py', old='    for shot_id in order:\\n        shot_dir = root / f"shot_{shot_id}"', new='    for shot_dir in sorted(root.glob(SHOT_DIR_GLOB)):\\n        shot_id = shot_dir.name[len("shot_") :]', caught_by='tests/test_bench_corpus.py', why="\`an/render.py\` concatenates in timeline order; a directory sort agrees only by luck, and when it does not every encode-side metric pairs one shot's source frames against another's decode."), Mutant(name='reshape_checks_divisibility_not_shape', file='an/bench/imageio.py', old='    if frames is not None and len(buf) != per_frame \* frames:', new='    if False:', caught_by='tests/test_bench_shape_guard.py', why='a k-times supersample makes the decoded buffer exactly k\*\*2 larger, so a divisibility check ALWAYS passes and family A is computed over k\*\*2 as many scrambled frames — plausibly, because at k=2 most horizontal runs survive the wrong reshape.'), Mutant(name='bench_measures_a_supersampled_render', file='an/bench/run.py', old='        if sizes != {capture.resolution}:', new='        if False:', caught_by='tests/test_bench_shape_guard.py', why="\`capture.resolution\` comes from the staged scene's meta and never from a file, so without an independent read of the PNGs' own IHDRs nothing in the pipeline ever compares the declared size to the size on disk."), Mutant(name='png_dimensions_trusts_a_non_ihdr_first_chunk', file='an/bench/png.py', old='    if data[_IHDR_TAG] != b"IHDR":', new='    if False:', caught_by='tests/test_bench_png.py', why='without the tag check the four bytes that happen to sit at offset 16 are returned as a resolution — a plausible number fed straight into the shape guard, which is the failure class an#54 closes.'), Mutant(name='read_png_dimensions_reads_the_whole_file', file='an/bench/png.py', old='        return png_dimensions(handle.read(PNG_HEADER_BYTES))', new='        return png_dimensions(handle.read())', caught_by='tests/test_bench_png.py', why='the answer stays right and the cost stops being free: the bench reads one of these per frame of every shot, and a 1080p frame is megabytes against a 24-byte header.'), Mutant(name='strict_exits_zero_on_a_row_it_cannot_read', file='an/tools.py', old='        if strict:\\n            print(refusal)', new='        if False:\\n            print(refusal)', caught_by='tests/test_bench_compare.py', why='the documented CI gate exited 0 on an unreadable schema_version or an undeclared --mutation — precisely the state a \`--strict --mutation supersample\` run is in before the lever is registered. Same class an#51 closed for the refusal path.'), Mutant(name='latest_rows_orders_by_filename', file='an/bench/compare.py', old='    return sorted(rows, key=key)[-count:]', new='    return sorted(rows, key=lambda p: p.name)[-count:]', caught_by='tests/test_bench_compare.py', why="filenames are <date>-<sha7>.json, so within one date the order is sha HEX order. A re-baseline and its after-run on the same day swap silently when the after-commit's sha sorts lower, and every improvement is then reported as a regression."), Mutant(name='compare_hides_that_a_row_was_blessed', file='an/bench/compare.py', old='            "blessed_scenes": sorted(after["provenance"].get("blessed") or ()),', new='            "blessed_scenes": [],', caught_by='tests/test_bench_compare.py', why="a bless run gates family B \`blessed_this_run\`, and \`format_comparison\` skips \`unchanged\` entries — so family B vanishes from the table entirely. 'Family B agreed' and 'family B was never asked' are the same blank space."), Mutant(name='capture_inherits_the_previous_renders_shots', file='an/bench/core_corpus.py', old='IGNORED_RELPATHS_ON_COPY: tuple[str, ...] = ("artifacts/shots",)', new='IGNORED_RELPATHS_ON_COPY: tuple[str, ...] = ()', caught_by='tests/test_bench_corpus.py', why="\`mall['shots']\` is \`<project>/artifacts/shots\`, and it is gitignored — so a previous render's per-shot mp4s cross into every bench run on a developer machine and on no clean checkout, in the module whose docstring is 'do not inherit a stale render'."), Mutant(name='capture_excludes_shots_by_basename_at_any_depth', file='an/bench/core_corpus.py', old='            n for n in names if prefix + n in IGNORED_RELPATHS_ON_COPY', new='            n\\n            for n in names\\n            if n in {p.rsplit("/", 1)[-1] for p in IGNORED_RELPATHS_ON_COPY}', caught_by='tests/test_bench_corpus.py', why="the obvious \`shutil.ignore_patterns('shots')\` spelling, restated. It fnmatches BASENAMES against the names in every directory, so it also deletes a character rig's \`assets/.../shots\` — and the other obvious spelling, \`'artifacts/shots'\` as a pattern, matches NOTHING, because no name contains a separator. Both fail silently."), Mutant(name='bless_names_its_row_after_the_tree_it_did_not_leave', file='an/bench/run.py', old='    return git_state(root) if blessed else git', new='    return git', caught_by='tests/test_bench_bless_protocol.py', why='\`git_state\` is read before the corpus loop and a \`--bless\` run writes inside it, so a bless on a clean tree filed itself as \`<date>-<sha>.json\` — a filename naming a commit whose tree that very run then modified, which is what the \`-dirty\` suffix exists to prevent.'), Mutant(name='golden_trusts_a_frame_its_own_record_disowns', file='an/bench/golden.py', old='        if expected is not None and expected != record["golden_sha256"]:', new='        if False:', caught_by='tests/test_bench_golden.py', why='the bless record and the committed PNG carry the same digest of the same file, written by two different calls. A disagreement means the golden is not the picture a human blessed — an edited file, a half-finished re-bless — and every one of those read as a clean PASS.'), Mutant(name='bench_asks_a_mutated_run_the_unmutated_question', file='an/tools.py', old='            compare_rows(load_row(compare), ledger, mutation=mutation or None)', new='            compare_rows(load_row(compare), ledger)', caught_by='tests/test_bench_mutation_cli.py', why="without the mutation, \`compare\` answers 'is the second row worse' of a run degraded on purpose — so the declared per-mutation predictions are never scored and the an#41 criterion cannot appear in the mandated \`--compare\` artifact at all."), Mutant(name='bench_blesses_a_deliberately_degraded_picture', file='an/tools.py', old='        if bless:\\n            return (\\n                "refusing --bless with --mutation: a lever renders a"', new='        if False:\\n            return (\\n                "refusing --bless with --mutation: a lever renders a"', caught_by='tests/test_bench_mutation_cli.py', why='blessing under a lever commits the degraded picture as the reference every future run is measured against — a permanent, silent re-baseline, and the one bless refusal that cannot be recovered by reading the recorded reason.'), Mutant(name='pix_fmt_knob_cannot_reach_the_encode', file='an/media/mp4.py', old='    resolved = pix_fmt or DEFAULT_PIX_FMT', new='    resolved = pix_fmt or "yuv420p"', caught_by='tests/test_encode_pins.py', why='reading the literal instead of the module global severs the seam any outside caller pulls — the same shape hoisting \`DETERMINISTIC_X264_ARGS\` into a default argument would sever for \`high_crf\`. That is why the seam is kept even though an#59 ships no lever — see the note there. (Until an#72 the row would ALSO have said 4:4:4 while the file stayed 4:2:0, because \`environment_record\` re-derived the format from the same global; it is measured off the delivered files now, so the row no longer lies about its own file — only the knob is broken.)'), Mutant(name='mux_argv_is_checked_by_subset_not_equality', file='an/media/mp4.py', old='        "-c:v",\\n        "libx264",\\n        "-pix_fmt",', new='        "-c:v",\\n        "libx264",\\n        "-tune",\\n        "animation",\\n        "-pix_fmt",', caught_by='tests/test_encode_pins.py', why='\`-tune animation\` is a measured-and-rejected flag (0.8%) and this is what adding it looks like. A SUBSET check passes — every pin is still present — and the encode moves and every encode-side metric is silently refused against every committed row. Only argv equality notices.'), Mutant(name='canvas_capture_flips_rows', file='an/stage/canvas_capture.py', old='rgb = image.convert("RGB")', new='rgb = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM).convert("RGB")', caught_by='tests/test_canvas_capture.py', why='the \`readPixels\` trap in reverse: WebGL readback is bottom-up and a PNG is top-down, so a capture path is one flip away from writing every frame upside down at exactly the declared size — past every shape check. The offline catcher is named here because a sweep runs the whole file per mutant; the browser equivalence gate (tests/test_canvas_capture_equivalence.py) catches the same flip in its own test.'), Mutant(name='capture_page_stops_compositing_the_canvas', file='an/stage/runtime/index.html', old='#stage { display: block; }', new='#stage { display: none; }', caught_by='tests/test_cutout_runtime_files.py', why="an#57's proposal. The element screenshot (the \`--capture screenshot\` path, which shares this page with the canvas default) is a PAGE capture clipped to the element, so hiding the canvas does not make it cheaper — it makes \`Locator.screenshot\` time out after 30 s per frame. The two spellings Playwright does accept return an all-white frame."), Mutant(name='supersample_autodensity_true', file='an/stage/runtime/runtime.js', old='            autoDensity: false,', new='            autoDensity: true,', caught_by='tests/test_bench_supersample_lever.py', why="\`autoDensity: true\` makes Chromium composite the k-times backbuffer down before the screenshot — a blind downscale with no filter choice and no record. The PNGs come out the DECLARED size, so every shape check passes and the whole knob silently measures nothing. It is the option whose name most suggests it is the right one. Lives on the PRODUCT's file since an#58, because the product owns the key."), Mutant(name='supersample_skips_the_frame_stage', file='an/bench/mutations.py', old='        capture.capture_frames = \_capture_then_resolve', new='        capture.capture_frames = original', caught_by='tests/test_bench_supersample_lever.py', why='drops the resolve, leaving k-times PNGs on disk. Before an#54 that was silent — \`_reshape\` checked byte-count divisibility and k\*\*2 always divides — and family A was computed on k\*\*2 scrambled frames that still produced a believable \`edge_transition_width\`. It is a loud refusal now, which is what makes this lever safe to run.'), Mutant(name='supersample_verify_is_merely_not_shipped', file='an/bench/mutations.py', old='    if recorded != expected:', new='    if False:', caught_by='tests/test_bench_supersample_lever.py', why="reduces the supersample fingerprint to \`disabled_aa\`'s inequality, which ANY render lever satisfies — both stage through one seam and both move \`render_side.runtime_sha256\`. A row rendered with \`antialias: false\` then verifies as a supersample row and the whole lever table is written from the wrong lever's numbers."), Mutant(name='edge_masked_colour_count_is_not_masked', file='an/bench/metrics.py', old='    per_frame = [len(np.unique(f[m])) for f, m in zip(packed, edge) if m.any()]', new='    per_frame = [len(np.unique(f)) for f, m in zip(packed, edge) if m.any()]', caught_by='tests/test_bench_metrics.py', why='unmasked it is \`frame_distinct_colours\` under a second name, and the one property the mask does buy — that an interior-only change cannot reach the number — is gone with no other symptom.'), Mutant(name='empty_edge_mask_reads_as_zero_colours', file='an/bench/metrics.py', old='        return float("nan"), 0', new='        return 0.0, 0', caught_by='tests/test_bench_metrics.py', why='a substituted zero is the largest possible DOWNWARD move in the one metric that exists to notice a downward move, on exactly the scenes where the number means nothing at all.'), Mutant(name='lossless_leg_pinned_to_420', file='an/bench/imageio.py', old='        "-pix_fmt",\\n        resolved,\\n        "-qp",', new='        "-pix_fmt",\\n        "yuv420p",\\n        "-qp",', caught_by='tests/test_bench_lossless_leg.py', why='a reference PINNED in the one dimension it has to track. The leg exists to be the plane libx264 received; \`-pix_fmt\` names what libx264 receives, so pinning it does not keep the reference lossless — it makes the reference a different colour pipeline from the delivered file, and every encode-side metric silently acquires the whole 4:2:0 conversion the reference exists to cancel. Distinct from every other entry here because the mutated code stays correct on the default path and is wrong only under a knob: measured on the corpus at 4:4:4, it changes the SIGN of family E on three of ten scenes (an#72).'), Mutant(name='sweep_never_finds_a_reversal', file='an/bench/compare.py', old='    unstable = tally["increase"] > 0 and tally["decrease"] > 0', new='    unstable = tally["increase"] > 0 and tally["decrease"] < 0', caught_by='tests/test_bench_compare.py', why="the robustness gate that stops being able to fire. Every row still carries its sweep and every report still prints a \`sweep\` block reading \`stable\`, so the instrument looks exactly like one that checked and found nothing — while \`graded_field\`'s +84.2% at tol 6, which is -81.6% at tol 8, counts toward family D again (an#140)."), Mutant(name='sweep_counts_a_different_statistic', file='an/bench/metrics.py', old='int((dev > t).sum())', new='int((dev >= t).sum())', caught_by='tests/test_bench_metrics.py', why="a sweep of a statistic the row does not report. Off by one code value, every cell stays a plausible, monotone survival count, and the comparer then certifies the robustness of \`>=\` while the ledger's number is \`>\` (an#140)."), Mutant(name='sweep_deletion_is_excused', file='an/bench/compare.py', old='    spec = declared.get("threshold_sweep")\\n', new='    spec = None\\n', caught_by='tests/test_bench_compare.py', why="a row that declares a threshold sweep and carries none reads as 'written before an#140' — so deleting one field from a row turns an \`unstable\` verdict back into a counted witness, the cheapest possible way to fake a caught mutation."), Mutant(name='strict_passes_an_unstable_movement', file='an/tools.py', old='else bool(report.get("has_regressions") or report.get("unstable"))', new='else bool(report.get("has_regressions"))', caught_by='tests/test_bench_compare.py', why="\`unstable\` is neither a regression nor a pass; with no mutation it means some cell of the metric's own grid got worse. A CI gate that exits 0 on it reads 'cannot tell' as 'fine' (an#140)."))*

A representative sweep rather than an exhaustive one, chosen so each entry
pins a *different* class of failure: a silently widened comparison, a guard
that reads the same table twice, a refusal that stops refusing, an
unknown-is-not-zero substitution, a decoder that only works on its own
output, and an instrument that goes blind without saying so.

### *class* an.bench.mutants.Mutant(name, file, old, new, caught_by, why)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One deliberate defect, and the guard that must notice it.

#### caught_by *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The test file that must go red. The whole file runs.

#### file *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Repo-relative path of the file to break.

#### old *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Exact source text to replace. Must occur **exactly once**.

### *exception* an.bench.mutants.MutantError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A declared mutant no longer applies, or the tree was left dirty.

### *exception* an.bench.mutants.MutantRunInterrupted

Bases: [`KeyboardInterrupt`](https://docs.python.org/3/builtins/exceptions.html#KeyboardInterrupt)

A terminating signal arrived mid-sweep, raised so the restore can run.

Derived from `KeyboardInterrupt` — a `BaseException` — rather than from
`Exception`, on purpose: an `except Exception` anywhere between here and
the top would swallow it and the sweep would carry on with a mutated file on
disk, which is the outcome the whole mechanism exists to prevent.

### an.bench.mutants.PYTEST_ARGS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('-q', '--no-header', '--tb=no', '-p', 'no:cacheprovider')*

see the module docstring. No
`--cache-provider` so a failed mutant cannot leave a `--lf` trail behind.

* **Type:**
  pytest flags for a mutant run. No `-k`

### an.bench.mutants.RESTORE_ON_SIGNALS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), ...]* *= (Signals.SIGTERM, Signals.SIGHUP)*

Terminating signals turned into an exception for the duration of a sweep, so
the restoring `finally` runs. SIGINT is deliberately absent: it already
raises `KeyboardInterrupt`, and re-handling it would only replace a working
mechanism. Built from what the platform actually has — Windows has no SIGHUP,
and asking for one is an `AttributeError` at import time.

### an.bench.mutants.RESTORE_TMP_SUFFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '.an-restore-tmp'*

Suffix for the sibling file the restore is staged through. Same directory, so
`os.replace` is a rename within one filesystem and therefore atomic.

### an.bench.mutants.SWEEP_COPY_IGNORE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('_\_pycache_\_', '.pytest_cache', '.ruff_cache', '.venv', 'node_modules', 'out')*

Left out of a sweep’s throwaway tree. Caches and build output only —
**\`.git\` IS copied**, because six of the thirteen guard files call
`dirty_paths` or `repo_root`, and a tree with no history fails them for the
wrong reason. Measured at 0.26 s for this repository, once per sweep.

### an.bench.mutants.check_sites(root=None)

Problems with the declarations themselves, as a list of sentences.

Separate from running them because it is nearly free and catches the failure
that matters most: a refactor moved the code, so a mutant no longer applies
and has silently stopped proving anything.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.bench.mutants.format_results(results)

The digest, with the survivors and errors last because they are the finding.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.bench.mutants.restore_on_termination(signals=(Signals.SIGTERM, Signals.SIGHUP))

Turn a terminating signal into an exception for the duration.

`run_mutants` restores in a `finally`, which covers everything that
*raises* — an exploding pytest, Ctrl-C — and covers nothing about SIGTERM,
which stops the interpreter without raising, so no `finally` runs and the
mutated file stays on disk. A `kill`, a timeout, an agent harness reaping a
background task and a closing terminal are all SIGTERM, and the sweep is
slow enough that interrupting it is the normal thing to do.

The previous handlers are restored on the way out, because this module is
importable and a library that permanently rewires SIGTERM is a worse defect
than the one it fixes. Yields the signals it actually took, which is empty
off the main thread (`signal.signal` refuses there), on a platform that
will not have them, and for any signal arriving already `SIG_IGN` — a
caller that wants to *report* the coverage can, and the restore itself never
depends on it.

An inherited `SIG_IGN` is left alone. `nohup` and most detach wrappers
ignore SIGHUP so a long job survives the terminal closing, and a sweep is
exactly the job someone detaches; taking the signal there would convert a
deliberately-protected run into a partial one exiting 130. There is nothing
to protect against either way — a signal that is ignored is never delivered,
so it cannot leave a mutation on disk.

**SIGKILL cannot be handled at all**, which is why the recovery path in
[`check_sites()`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.check_sites) is the load-bearing half of this fix and this is the
convenience.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]]

### an.bench.mutants.run_mutants(names=None, , root=None)

Apply each mutant, run its whole guard file, restore, and report.

\*\*With no `root`, the sweep runs against a COPY of the repository\*\* and the
real working tree is never written (an#124). See [`sweep_tree()`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.sweep_tree) for why
that is not merely tidiness. An explicit `root` is swept IN PLACE, because a
caller who names a tree has already chosen a throwaway.

Restoration is in a `finally` and rewrites the ORIGINAL text rather than
reversing the substitution: a reversal that itself failed would leave the
tree broken, which is a worse outcome than any mutant surviving.

A `finally` covers everything that *raises*, which is why the loop runs
inside [`restore_on_termination()`](_autosummary/an.bench.mutants.html.md#an.bench.mutants.restore_on_termination): SIGTERM does not raise. What no
handler can cover is SIGKILL, so `check_sites` — which runs first, here —
also recognises a file left mutated by a previous kill and says so in those
words rather than as declaration rot.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### an.bench.mutants.sweep_tree(source)

A throwaway copy of `source` for a sweep to do its damage in.

\*\*A sweep mutates real source files, and every mutation here is chosen to be
plausible\*\* — it compiles, it renders, and the suite stays green apart from
the one test that names it. So a concurrent reader of the working tree does
not get an error, it gets a believable wrong answer. That reader is not
hypothetical: `test_a_representative_mutant_is_really_caught` runs in the
DEFAULT leg, so a plain `pytest -q` mutates `an/bench/compare.py` for a few
seconds, and a second suite, an editor re-indexing, a `git status` from
another shell or a lint job sees it (an#124 — three pytest processes against
one checkout, and which of them owned the file’s contents was unanswerable).

Copying is what makes that structurally impossible rather than merely
coordinated. A lock would order the *writers*; it cannot reach a reader that
never took it.

It also retires an#67’s hazard for this path: a sweep killed by SIGKILL can
now only leave a mutated file inside a temp directory that nothing reads.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### an.bench.mutants.verdict_of(result)

`CAUGHT` / `SURVIVED` / `ERRORED`, as three separate answers.

`ERRORED` is not a third flavour of caught: a mutant that stops the guard
file from being collected has demonstrated nothing about the guard.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.bench.mutations.html.md

# an.bench.mutations

The levers: deliberate, declared changes through seams the shipped code already has.

This is the other half of the instrument. `an bench` records numbers; these
change the pipeline on purpose so a test can check the numbers move the way the
registry declared **in advance**. A metric that never moves under any lever is
decoration, and the only way to know which is which is to pull one.

**Two of the three are degradations and the third is an improvement**, and that
asymmetry is the point rather than an untidiness. `high_crf` and
`disabled_aa` make the picture worse; `supersample` makes it better. A panel
that has only ever been shown things getting worse cannot tell an improvement
from a regression — run as a plain commit-to-commit diff, a k=2 supersample
reports **2 false regressions** (`off_palette_pixel_fraction` rises as blends
multiply, `min_ssim_win8_vs_golden` falls away from the golden) and \*\*7
unearned improvements\*\* (every family C/D/E/G metric whose mask derives from the
source frames: gates live inside `Prediction`, which exists only per declared
mutation, so with `mutation=None` no gate is consulted and a softer source
shrinks each mask to its easiest members). Declared as a lever, none of that
happens. So what a lever has to be is **declared in advance**, not bad — and the
word “degradations” in the old first line was quietly making the stronger, false
claim (an#56).

**Two of the three levers have no production knob, and that is deliberate.**
The third, `supersample`, has had one since an#58 (`an render --supersample
N`) and the lever now FORCES the product’s own parameter rather than carrying
a second copy of the resolve — a lever that reproduces the code it examines is
examining itself. The inverse also exists: `step_hz` (an#89) is a product
knob with **no lever**, on measurement — a per-frame instrument cannot judge a
temporal choice (the `an-dev-bench` skill’s table has the numbers). Each lever
reaches an existing seam from the outside:

- `high_crf` rebinds `an.stage.render.DETERMINISTIC_X264_ARGS`.
  `_ffmpeg_mux` reads that name as a module global at call time, so the
  rebinding reaches the delivered encode. It does **not** reach
  `an.bench.imageio.lossless_encode_command`, which bound the tuple at import
  — and that is exactly right: the lossless reference must stay lossless, or
  every encode-side metric would be measured against a moving target and the
  lever would produce beautiful numbers about nothing.
- `disabled_aa` copies the staged runtime, flips PixiJS’s `antialias` in the
  copy, and rebinds `an.stage.render.runtime_dir`. The shipped
  `runtime.js` is never written to.
- `supersample` reaches the SAME runtime seam — `resolution: k,
  autoDensity: false` in the Pixi application options — and then a second one
  it cannot do without: it rebinds `an.engines.capture.capture_frames` (the
  core frame stage’s capture loop since an#247; before it, the stage’s own
  `_capture_frames`) so the k-times PNGs are block-mean-resolved back to the
  declared size **in the frame stage**, before ffmpeg or the metrics or the
  golden gate read them. That is not tidiness. A
  lever must measure what the product will produce, and everything downstream
  reads the declared resolution off the STAGED SCENE, never off the files.

A knob in the product is a cost each of the other two levers avoids: it has to
be documented, defended, and kept from being switched on by accident — which is
why supersampling is opt-in and off is free.

**Each lever verifies that it applied.** A lever that silently failed to take
produces a run in which nothing moved — which reads exactly like an instrument
that cannot see it, and sends the reader to fix the wrong thing. So
`verify_applied` is part of the declaration rather than a courtesy, and the
two levers verify it in different places for a structural reason: the encode
argv is recorded in the ledger row, and the runtime is not (the runtime is the
code under test, not a comparability key — see
`an.bench.registry.MUTATION_TOUCHES`).

### Module Attributes

| [`HIGH_CRF`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.HIGH_CRF)       | measured on the CRF ladder, 40 gives C x8.4, D x8.0 and F -42% on `single_character` — large, unambiguous, and still a rate a human might plausibly ship.                                                                         |
|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`AA_ON`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.AA_ON)          | The exact text the AA lever flips, and where.                                                                                                                                                                                     |
| [`SUPERSAMPLE_K`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.SUPERSAMPLE_K)  | The supersample lever's factor, read at call time so a test can move it.                                                                                                                                                          |
| [`APP_OPEN`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.APP_OPEN)       | The exact text the supersample lever anchors to, and what it inserts.                                                                                                                                                             |
| [`STAGING_IGNORE`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.STAGING_IGNORE) | Excluded from the staged copy so the staged tree is a pure function of the shipped source and the patch — which is what lets `_verify_supersample` RECOMPUTE the digest it expects instead of settling for "not the shipped one". |
| [`LEVERS`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.LEVERS)         | The levers, keyed by the mutation name the registry declares.                                                                                                                                                                     |

### Functions

| [`mutated_row`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.mutated_row)(name, \*\*run_kwargs)   | Render the corpus with one lever pulled, and prove the lever took.   |
|--------------------------------------------------------------------------------------|----------------------------------------------------------------------|

### Classes

| [`Lever`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.Lever)(name, side, what, why, apply[, verify_row])   | One deliberate, declared change to the pipeline, with the evidence that it took.   |
|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|

### Exceptions

| [`MutationError`](_autosummary/an.bench.mutations.html.md#an.bench.mutations.MutationError)   | A lever could not be applied, or applied and left no trace.   |
|------------------------------------------------------------------|---------------------------------------------------------------|

### an.bench.mutations.AA_ON *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'antialias: true'*

The exact text the AA lever flips, and where. Pinned as a literal so a
rename in `runtime.js` fails here — loudly, at the lever — rather than
producing a “mutation” that changes nothing.

### an.bench.mutations.APP_OPEN *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'const resolution = Math.max(1, (NS.anSupersample | 0) || 1);'*

The exact text the supersample lever anchors to, and what it inserts. Pinned
for the same reason `AA_ON` is: a reformat of the Pixi options object must
fail here rather than produce a “mutation” that renders at 1x and then reads
as an instrument that cannot see a supersample. `app = new PIXI.Application({`
and NOT `new PIXI.Application(`: the shorter form occurs twice in
`runtime.js` — the second is inside a comment — and a two-hit anchor would
patch prose.

**\`autoDensity: false\` is load-bearing and is the whole plumbing finding.**
With it `true`, Pixi sets the canvas CSS size to the LOGICAL size and
Chromium composites the k-times backbuffer down before the screenshot — a
blind browser downscale with no filter choice and no record of having
happened, i.e. the `device_scale_factor` failure wearing the name that most
suggests it is the right one. Measured on `aa_probe`, declared 320x240:
neither key -> 320x240 PNGs; `resolution: 2, autoDensity: false` -> 640x480;
`resolution: 2, autoDensity: true` -> 320x240. Neither key is in the shipped
options today, so the engine default `RESOLUTION: 1` applies silently and
both have to be introduced.
Since an#58 the product owns `resolution` / `autoDensity: false` and reads
the factor from an injected global, so the lever \*\*overrides the line that
reads it\*\* rather than writing a second copy of the product’s Pixi options.
A lever that reproduces the code it is examining is examining itself.

Injecting the global does NOT work and the reason is worth keeping: the
product sets `window.anSupersample` from `ctx.supersample` immediately before
`anLoadScene`, so it would overwrite whatever the lever put there. Caught by
an#54’s shape guard — 160x120 frames against a 320x240 declaration — which is
what that guard is for.

### an.bench.mutations.HIGH_CRF *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '40'*

measured on the CRF
ladder, 40 gives C x8.4, D x8.0 and F -42% on `single_character` — large,
unambiguous, and still a rate a human might plausibly ship. 51 is
pathological, and a lever nobody would ever pull by accident is a weaker
proxy for the regressions this instrument exists to catch.

* **Type:**
  The CRF the encoder lever raises to. 40 rather than 51

### an.bench.mutations.LEVERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Lever](_autosummary/an.bench.mutations.html.md#an.bench.mutations.Lever)]* *= {'disabled_aa': Lever(name='disabled_aa', side='render', what='build the PixiJS application with multisampling off', why='moves the render-side families and fires the golden tripwire. Its effect is scene-dependent by measurement, not by accident: MSAA applies to WebGL geometry, so an SVG sprite is nearly blind to it (96 differing pixels of 12.4M) and axis-aligned \`drawRect\` edges are bit-identical with it on or off. \`aa_probe\` exists so the corpus contains edges this lever can actually change.', apply=<function \_disabled_aa>, verify_row=<function \_verify_disabled_aa>), 'high_crf': Lever(name='high_crf', side='encode', what='raise the delivered encode from the pinned CRF to 40', why='moves only post-encode metrics. The golden corpus is upstream of the encoder, so family B cannot see this by construction — which is the reason two disjoint levers are mandatory.', apply=<function \_high_crf>, verify_row=<function \_verify_high_crf>), 'supersample': Lever(name='supersample', side='render', what='build the PixiJS application at resolution 2 with \`autoDensity: false\`, and resolve the frames back to the declared size with an exact 2x2 block mean before anything reads them', why="the instrument's exam against a change somebody WANTS to ship (an#56). Run instead as a plain commit-to-commit diff, \`_verdict_by_optimum\` reports it as 2 false regressions and 7 unearned improvements, plus 7 unscored \`changed\`s including the metric the wave's done-when names — a table that looks like evidence and is not. Its effect is scene-dependent BY MEASUREMENT and in the exact inverse of \`disabled_aa\`: +2.6% to +8.0% edge width on the five procedural scenes and -34.8% on \`promote_demo\`. The two render levers therefore reach complementary scenes, which strengthens the harness rather than diluting it.", apply=<function \_supersample>, verify_row=<function \_verify_supersample>)}*

The levers, keyed by the mutation name the registry declares. At least one
per SIDE is mandatory and the two sides are **disjoint on purpose**: an
encoder lever cannot touch a golden-frame metric, because the corpus sits
UPSTREAM of the encoder. So requiring three families from a CRF change alone
would fail for a reason that has nothing to do with the instrument being
blind — and that failure would be misdiagnosed as the harness being wrong.

The two RENDER levers are not redundant: measured, they reach complementary
scenes. `disabled_aa` is nearly blind to the descriptor path (96 differing
pixels of 12.4M on `promote_demo`, because MSAA applies to WebGL geometry and
an SVG sprite is a pre-rasterised texture); `supersample` hits that same
scene hardest of all six (-34.8% edge width, because the sprite rasterises AT
2x rather than being stretched up from a 1x texture).

### *class* an.bench.mutations.Lever(name, side, what, why, apply, verify_row=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One deliberate, declared change to the pipeline, with the evidence that it took.

#### verify_row *: [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[dict](https://docs.python.org/3/builtins/stdtypes.html#dict)], [None](https://docs.python.org/3/builtins/constants.html#None)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Given the ledger row the mutated run produced, raise unless the lever’s
fingerprint is in it. `None` when the row cannot carry one — see the
module docstring.

### *exception* an.bench.mutations.MutationError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A lever could not be applied, or applied and left no trace.

### an.bench.mutations.STAGING_IGNORE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('_\_pycache_\_',)*

Excluded from the staged copy so the staged tree is a pure function of the
shipped source and the patch — which is what lets `_verify_supersample`
RECOMPUTE the digest it expects instead of settling for “not the shipped
one”. `runtime_sha256()` walks whatever `render.runtime_dir()` returns, so
with this excluded on the staging side and on the recompute side, the two
hash byte-identical file sets.

### an.bench.mutations.SUPERSAMPLE_K *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 2*

The supersample lever’s factor, read at call time so a test can move it. 2
rather than 3, deliberately and with the residual on the record: research §3
renders each corpus scene at rising k and lets `edge_transition_width`
converge, and k=3 reaches that ceiling on every scene that has one while k=2
falls 43% short on `saturated_outline` and 33% short on `graded_field`
(0.09-0.17 px). It is 2 because \*\*the lever must be the change the product
will ship\*\* — an#58 ships k=2 — and a lever measuring a factor nobody will
run is a beautiful number about nothing. Cost, read off 1080p because the
corpus cannot inform it: 0.126 s/f at k=1, 0.319 at k=2 (2.54x), 0.640 at k=3.

### an.bench.mutations.mutated_row(name, \*\*run_kwargs)

Render the corpus with one lever pulled, and prove the lever took.

Deliberately returns a row rather than a comparison: what to do with it is
[`an.bench.compare`](_autosummary/an.bench.compare.html.md#module-an.bench.compare)’s job, and keeping the two apart is what lets the
criterion be evaluated against a row written months ago.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/an.bench.palette.html.md

# an.bench.palette

Derive the set of colours a compiled shot declared — never hand-pin it.

`off_palette_pixel_fraction`’s whole meaning is “not one of the colours the
compiler declared for this shot”. A hand-tuned palette constant would make it a
per-scene magic number, which is exactly what killed its predecessor
(`blend_pixel_fraction`: a 12-flat-fill, anti-aliasing-free frame read 2.46%
at `K=10`, i.e. 4.8x the entire claimed signal).

So the palette is read out of the artifacts the browser actually loaded — the
staged `scene.json` and the staged SVG files beside it — never from a
re-compile. A re-compile can differ from what ran: a missing character
descriptor makes the compiler fall back to the procedural rig, and before
an#33 it did so without a word.

**Three things this module must get right, each a silent failure otherwise:**

1. `parse_color` is a **verbatim mirror of the runtime’s rule**
   (`hex.padEnd(6,'0').slice(0,6)`), not a CSS parser. A CSS-correct 3-digit
   expander maps `"#222"` to `0x222222`; the runtime paints `0x222000`.
2. Some colours are **runtime constants, never present in the JSON** — the eye
   whites, the four mouth colours. A JSON-only sweep under-collects them and
   the metric reads high with no error anywhere.
3. Some `visual.color` values are **inert**: `drawMouthShape` never reads
   its node’s colour, and every `svg_sprite` carries the `#888888` schema
   default. Collecting those over-collects.

Both directions are recorded in `palette_sources` so a reviewer can see which
half moved when the number does.

### Module Attributes

| [`RUNTIME_DEFAULT_COLOUR`](_autosummary/an.bench.palette.html.md#an.bench.palette.RUNTIME_DEFAULT_COLOUR)    | The runtime's fallback when a `visual.color` is absent or not a string.      |
|----------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`RUNTIME_EYE_COLOURS`](_autosummary/an.bench.palette.html.md#an.bench.palette.RUNTIME_EYE_COLOURS)       | the sclera fill and the 0.6-alpha outline.                                   |
| [`RUNTIME_MOUTH_COLOURS`](_autosummary/an.bench.palette.html.md#an.bench.palette.RUNTIME_MOUTH_COLOURS)     | lip, fill, teeth, tongue.                                                    |
| [`RUNTIME_EYE_PUPIL_DEFAULT`](_autosummary/an.bench.palette.html.md#an.bench.palette.RUNTIME_EYE_PUPIL_DEFAULT) | The runtime's fallback pupil colour when a `visual.color` is absent.         |
| [`COLOURED_KINDS`](_autosummary/an.bench.palette.html.md#an.bench.palette.COLOURED_KINDS)            | `visual.kind` values whose `visual.color` the runtime actually paints.       |
| [`INERT_COLOUR_KINDS`](_autosummary/an.bench.palette.html.md#an.bench.palette.INERT_COLOUR_KINDS)        | `visual.kind` values whose `visual.color` is inert.                          |
| [`RUNTIME_JS_RELPATH`](_autosummary/an.bench.palette.html.md#an.bench.palette.RUNTIME_JS_RELPATH)        | Where the runtime's own hard-coded colours live, for the source cross-check. |

### Functions

| [`palette_for_scene`](_autosummary/an.bench.palette.html.md#an.bench.palette.palette_for_scene)(scene_json, \*, runtime_dir)   | The declared palette of one staged shot.                                        |
|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`parse_color`](_autosummary/an.bench.palette.html.md#an.bench.palette.parse_color)(value)                               | The runtime's own colour rule, mirrored exactly.                                |
| [`runtime_literal_colours`](_autosummary/an.bench.palette.html.md#an.bench.palette.runtime_literal_colours)(runtime_js)              | Every 6-digit hex literal the runtime source paints.                            |
| [`svg_colours`](_autosummary/an.bench.palette.html.md#an.bench.palette.svg_colours)(svg_path)                            | Every colour literal an SVG paints, plus the tokens that could not be resolved. |

### an.bench.palette.COLOURED_KINDS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'ellipse', 'path', 'rect'})*

`visual.kind` values whose `visual.color` the runtime actually paints.
A stroked path (an#160) paints `path.color` — the compiler stamps the same
value onto `visual.color` (asserted by `tests/test_styles.py`), so one read
covers the stroke and the arrowhead, which is filled in that colour.

### an.bench.palette.INERT_COLOUR_KINDS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'mouth', 'sprite', 'svg_sprite'})*

`visual.kind` values whose `visual.color` is inert.

### an.bench.palette.RUNTIME_DEFAULT_COLOUR *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 8947848*

The runtime’s fallback when a `visual.color` is absent or not a string.

### an.bench.palette.RUNTIME_EYE_COLOURS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), ...]* *= (16777215, 2236962)*

the sclera fill and the
0.6-alpha outline. The outline’s alpha means it paints BLENDS, so it is a
lower bound on that node’s contribution — the safe direction.

* **Type:**
  Painted by `makeEye` regardless of `visual.color`

### an.bench.palette.RUNTIME_EYE_PUPIL_DEFAULT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '#1a1a1a'*

The runtime’s fallback pupil colour when a `visual.color` is absent.

### an.bench.palette.RUNTIME_JS_RELPATH *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an/stage/runtime/runtime.js'*

Where the runtime’s own hard-coded colours live, for the source cross-check.

### an.bench.palette.RUNTIME_MOUTH_COLOURS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), ...]* *= (7023403, 2756624, 16448250, 11552840)*

lip, fill, teeth, tongue. The mouth node’s own
`visual.color` is never read.

* **Type:**
  Painted by `drawMouthShape`

### an.bench.palette.palette_for_scene(scene_json, , runtime_dir)

The declared palette of one staged shot.

`scene_json` is the **staged** compiled scene (what the browser loaded);
`runtime_dir` is the directory it was served from, which is where its
SVGs were staged.

Returns a dict carrying the palette itself plus enough provenance to
explain a move: which half of the derivation each colour came from, whether
the palette is a superset of what was painted, and every token that could
not be resolved.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.palette.parse_color(value)

The runtime’s own colour rule, mirrored exactly.

`runtime.js`:

```default
function parseColor(s) {
    if (typeof s !== 'string') return 0x888888;
    const hex = s.startsWith('#') ? s.slice(1) : s;
    return parseInt(hex.padEnd(6, '0').slice(0, 6), 16);
}
```

Note what that is **not**: a 3-digit CSS shorthand expander. `"#222"`
pads to `"222000"`, so the runtime paints `0x222000` and so must this.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> hex(parse_color("#222"))
'0x222000'
>>> hex(parse_color("#ffffffaa"))
'0xffffff'
>>> hex(parse_color(None))
'0x888888'
```

### an.bench.palette.runtime_literal_colours(runtime_js)

Every 6-digit hex literal the runtime source paints.

Used to cross-check [`RUNTIME_EYE_COLOURS`](_autosummary/an.bench.palette.html.md#an.bench.palette.RUNTIME_EYE_COLOURS) and
[`RUNTIME_MOUTH_COLOURS`](_autosummary/an.bench.palette.html.md#an.bench.palette.RUNTIME_MOUTH_COLOURS) against the file that actually paints them,
so adding a fifth mouth colour reddens a test instead of silently inflating
`off_palette_pixel_fraction`.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

### an.bench.palette.svg_colours(svg_path)

Every colour literal an SVG paints, plus the tokens that could not be resolved.

Parsed as XML rather than scraped with a regex, so `style="fill:#abc"`
and a `display:none` subtree are both handled — the first is a spelling a
regex over `fill="…"` misses entirely, and the second contributes colours
to a palette that never reach a pixel.

An unresolvable token (a named colour, a `url(#gradient)` reference) is
**returned rather than guessed**: guessing puts a wrong colour in the
palette, and the metric then reads low with no error anywhere.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`int`](https://docs.python.org/3/builtins/functions.html#int)], [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]


# _autosummary/an.bench.paths.html.md

# an.bench.paths

Where the bench reads its corpus from and writes its ledger to.

One module owns every path so the “bench needs a source checkout” constraint is
stated once, with a typed error, rather than surfacing as a `FileNotFoundError`
from inside `shutil.copytree`.

### Module Attributes

| [`LEDGER_DIRNAME`](_autosummary/an.bench.paths.html.md#an.bench.paths.LEDGER_DIRNAME)   | Ledger rows live here, one file per (date, commit).                                                                                        |
|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| [`GOLDEN_DIRNAME`](_autosummary/an.bench.paths.html.md#an.bench.paths.GOLDEN_DIRNAME)   | Golden frames (an#38 fills this; the path convention ships now so the cassette work and the corpus work do not have to agree on it later). |

### Functions

| [`git_state`](_autosummary/an.bench.paths.html.md#an.bench.paths.git_state)([root])                                 | `sha` / `branch` / `dirty` for the checkout, or <br/><br/>```<br/>``<br/>```<br/><br/>None\`\`s off-git.   |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| [`golden_dir`](_autosummary/an.bench.paths.html.md#an.bench.paths.golden_dir)([root])                                | The golden-frame directory (an#38).                                                                        |
| [`golden_path`](_autosummary/an.bench.paths.html.md#an.bench.paths.golden_path)(scene, frame_key, chromium_build, \*) | Where one golden frame lives.                                                                              |
| [`ledger_dir`](_autosummary/an.bench.paths.html.md#an.bench.paths.ledger_dir)([root])                                | The ledger directory, created if absent.                                                                   |
| [`ledger_path`](_autosummary/an.bench.paths.html.md#an.bench.paths.ledger_path)(\*[, root, git])                      | `<date>-<sha>[-dirty].json`.                                                                               |
| [`repo_root`](_autosummary/an.bench.paths.html.md#an.bench.paths.repo_root)()                                       | The source checkout `an` was imported from.                                                                |

### Exceptions

| [`BenchLayoutError`](_autosummary/an.bench.paths.html.md#an.bench.paths.BenchLayoutError)   | The bench was run somewhere it cannot find the corpus.   |
|---------------------------------------------------------------------|----------------------------------------------------------|

### *exception* an.bench.paths.BenchLayoutError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The bench was run somewhere it cannot find the corpus.

### an.bench.paths.GOLDEN_DIRNAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'misc/bench/golden'*

Golden frames (an#38 fills this; the path convention ships now so the
cassette work and the corpus work do not have to agree on it later).

### an.bench.paths.LEDGER_DIRNAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'misc/bench/ledger'*

Ledger rows live here, one file per (date, commit). Append-only by
convention: an existing row is evidence about a commit, and editing it
rewrites history that `an bench --compare` (an#40) reads as fact.

### an.bench.paths.git_state(root=None)

`sha` / `branch` / `dirty` for the checkout, or 

```
``
```

None\`\`s off-git.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.paths.golden_dir(root=None)

The golden-frame directory (an#38).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.bench.paths.golden_path(scene, frame_key, chromium_build, , root=None)

Where one golden frame lives.

Keyed on the **Chromium build alone** — no platform or arch segment. The
cross-architecture verdict measured those segments to be inert (zero
differing pixels and zero differing PNG bytes across arm64 macOS, x86-64
Linux and arm64 Linux, across two different SwiftShader JIT backends), so
carrying them would force one committed copy per platform for no
information. What the convention keeps is its real benefit: a Playwright
bump becomes a **new path requiring a deliberate re-bless** rather than a
red test with no explanation.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> golden_path("s", "f0", "140.0.7339.16").name
'f0-chromium140.0.7339.16.png'
```

### an.bench.paths.ledger_dir(root=None)

The ledger directory, created if absent.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.bench.paths.ledger_path(, root=None, git=None)

`<date>-<sha>[-dirty].json`.

The `-dirty` suffix is not decoration: a row measured against uncommitted
edits describes no commit, and a filename that claims one would be read by
an#40 as that commit’s evidence.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> ledger_path(git={"sha": "abc1234def", "dirty": True}).name.endswith("-dirty.json")
True
```

### an.bench.paths.repo_root()

The source checkout `an` was imported from.

Raises rather than returning a plausible-but-wrong path, because the
failure it guards is running the bench against an installed wheel: the
corpus lives under `misc/bench/`, which is not packaged, so the first
symptom would be a missing-fixture error three frames deep.

The checkout’s folder name is not asserted: a git worktree or a clone
under another name is still a source checkout.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> (repo_root() / "an" / "bench" / "paths.py").is_file()
True
```


# _autosummary/an.bench.png.html.md

# an.bench.png

A filter-0 PNG writer and a full-filter reader — numpy and stdlib only.

The golden corpus is committed as PNGs, and the gate compares \*\*decoded
pixels\*\*. Both halves of that sentence need this module.

**Why write our own encoder rather than copy Chromium’s bytes.** A committed
golden written by copying the screenshot file is a function of Chromium’s
libpng settings as well as of the picture. Re-encoding every row with filter
type 0 makes the committed file a function of the **pixel data alone**, so a
future Chromium bump that changes its filter heuristic produces no diff at all
when the picture has not moved. Measured on the five real frames in this repo,
filter-0 re-encoding is also *smaller* every time — between -5.2% and -22.3%
(research §3 recorded -0.30%, from a single frame; the range is wider both ways
than one sample suggested).

**Why the reader must handle all five filter types.** The bless path reads
Chromium’s own screenshots, and Chromium emits Sub / Up / Paeth. Only the files
this module writes are filter-0.

\*\*Why the filters are unfiltered over a `bytearray` rather than a numpy
array.\*\* Sub, Average and Paeth are sequential along the row — byte *x* depends
on byte *x - bpp* of the same row — so they cannot be vectorised, and the inner
loop is scalar either way. Scalar indexing into a `bytearray` is measurably
cheaper than into a numpy array: 32 ms against 111 ms for a 320x240 Chromium
frame, and 444 ms against 2,430 ms for a mixed-filter 1920x1080 one. A filter-0
file skips the loop entirely (a whole-row slice), which is why reading a golden
costs ~0.1 ms at 320x240 and ~1.8 ms at 1080p.

The round trip is exact, and it is verified in both directions against an
independent decoder: `ffmpeg -pix_fmt rgb24` agrees with [`decode_png()`](_autosummary/an.bench.png.html.md#an.bench.png.decode_png)
on every rendered frame in the repo, and reads back what [`encode_png()`](_autosummary/an.bench.png.html.md#an.bench.png.encode_png)
writes.

### Module Attributes

| [`PNG_SIGNATURE`](_autosummary/an.bench.png.html.md#an.bench.png.PNG_SIGNATURE)       | PNG's fixed 8-byte signature.                                                                                                   |
|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| [`DFLT_ZLIB_LEVEL`](_autosummary/an.bench.png.html.md#an.bench.png.DFLT_ZLIB_LEVEL)     | zlib level for the IDAT stream.                                                                                                 |
| [`RGB_CHANNELS`](_autosummary/an.bench.png.html.md#an.bench.png.RGB_CHANNELS)        | What [`encode_png()`](_autosummary/an.bench.png.html.md#an.bench.png.encode_png) writes, and the only channel count the golden gate compares. |
| [`SUPPORTED_BIT_DEPTH`](_autosummary/an.bench.png.html.md#an.bench.png.SUPPORTED_BIT_DEPTH) | Bit depth.                                                                                                                      |
| [`PNG_HEADER_BYTES`](_autosummary/an.bench.png.html.md#an.bench.png.PNG_HEADER_BYTES)    | the 8-byte signature, a 4-byte chunk length, the 4-byte `IHDR` tag, and two big-endian uint32s.                                 |

### Functions

| [`decode_png`](_autosummary/an.bench.png.html.md#an.bench.png.decode_png)(data)                  | Decode an 8-bit truecolour PNG to `(H, W, C)` uint8, `C` in `{3, 4}`.         |
|------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`encode_png`](_autosummary/an.bench.png.html.md#an.bench.png.encode_png)(rgb, \*[, level])      | Encode `(H, W, 3)` uint8 as an 8-bit truecolour PNG, every row filter 0.      |
| [`png_dimensions`](_autosummary/an.bench.png.html.md#an.bench.png.png_dimensions)(data)              | `(width, height)` from a PNG's IHDR, without decoding a single pixel.         |
| [`read_png`](_autosummary/an.bench.png.html.md#an.bench.png.read_png)(path)                    | `(H, W, 3)` uint8 for a PNG on disk, alpha dropped only if opaque.            |
| [`read_png_dimensions`](_autosummary/an.bench.png.html.md#an.bench.png.read_png_dimensions)(path)         | `(width, height)` for a PNG on disk, reading only its header.                 |
| [`to_rgb`](_autosummary/an.bench.png.html.md#an.bench.png.to_rgb)(arr)                       | Drop a **fully opaque** alpha channel, refusing to drop a meaningful one.     |
| [`write_png`](_autosummary/an.bench.png.html.md#an.bench.png.write_png)(path, rgb, \*[, level]) | Write `rgb` as a filter-0 PNG and **verify the round trip** before returning. |

### Exceptions

| [`PngFormatError`](_autosummary/an.bench.png.html.md#an.bench.png.PngFormatError)   | A PNG this module deliberately does not decode, or a malformed one.   |
|-------------------------------------------------------------------|-----------------------------------------------------------------------|

### an.bench.png.DFLT_ZLIB_LEVEL *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 9*

zlib level for the IDAT stream. 9 because a golden is written rarely and
read often, and because the committed bytes are reviewed in a diff.

### an.bench.png.PNG_HEADER_BYTES *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 24*

the 8-byte signature, a
4-byte chunk length, the 4-byte `IHDR` tag, and two big-endian uint32s. PNG
requires IHDR to be the FIRST chunk, so this prefix is always enough.

* **Type:**
  Bytes needed to read an image’s declared size

### an.bench.png.PNG_SIGNATURE *: [bytes](https://docs.python.org/3/builtins/stdtypes.html#bytes)* *= b'\\x89PNG\\r\\n\\x1a\\n'*

PNG’s fixed 8-byte signature.

### *exception* an.bench.png.PngFormatError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A PNG this module deliberately does not decode, or a malformed one.

Typed and specific on purpose: the alternative to refusing is returning a
plausible array, and a golden gate that compares a plausible array is worse
than one that does not run.

### an.bench.png.RGB_CHANNELS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 3*

What [`encode_png()`](_autosummary/an.bench.png.html.md#an.bench.png.encode_png) writes, and the only channel count the golden gate
compares. Alpha is dropped at the boundary (see [`to_rgb()`](_autosummary/an.bench.png.html.md#an.bench.png.to_rgb)), never here.

### an.bench.png.SUPPORTED_BIT_DEPTH *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 8*

Bit depth. 16-bit PNGs are refused rather than truncated — a silently
halved code value is exactly the class of bug this corpus exists to catch.

### an.bench.png.decode_png(data)

Decode an 8-bit truecolour PNG to `(H, W, C)` uint8, `C` in `{3, 4}`.

Refuses — rather than approximates — 16-bit, palette, greyscale and
interlaced images, naming what it found.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> a = np.array([[[1, 2, 3], [4, 5, 6]]], np.uint8)
>>> np.array_equal(decode_png(encode_png(a)), a)
True
```

### an.bench.png.encode_png(rgb, , level=9)

Encode `(H, W, 3)` uint8 as an 8-bit truecolour PNG, every row filter 0.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> import numpy as np
>>> data = encode_png(np.zeros((2, 3, 3), np.uint8))
>>> data[:8] == PNG_SIGNATURE
True
>>> decode_png(data).shape
(2, 3, 3)
```

### an.bench.png.png_dimensions(data)

`(width, height)` from a PNG’s IHDR, without decoding a single pixel.

Width first, matching the PNG header itself — and deliberately the opposite
order from [`read_png()`](_autosummary/an.bench.png.html.md#an.bench.png.read_png), which returns `(H, W, C)` like every other
array here. Getting it backwards produces a square-looking check that
passes on square frames only, so the order is asserted in the tests.

Cheap on purpose. The bench reads this for **every** frame on disk rather
than sampling one, because the failure it exists to catch — a render whose
frame size changed partway through — is exactly the one sampling misses.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> import numpy as np
>>> png_dimensions(encode_png(np.zeros((240, 320, 3), np.uint8)))
(320, 240)
```

### an.bench.png.read_png(path)

`(H, W, 3)` uint8 for a PNG on disk, alpha dropped only if opaque.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.bench.png.read_png_dimensions(path)

`(width, height)` for a PNG on disk, reading only its header.

A 1080p frame is megabytes and its declared size is in the first 24 of
them. Reading only those is what makes checking every frame of every shot
free rather than a second full decode of the corpus.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### an.bench.png.to_rgb(arr)

Drop a **fully opaque** alpha channel, refusing to drop a meaningful one.

The golden gate compares RGB ([`an.bench.metrics.golden_comparison()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.golden_comparison)
takes three channels). Dropping alpha unconditionally would make a
transparency regression invisible to the one gate that exists to see
changes, so a non-opaque alpha is an error rather than a silent narrowing.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> to_rgb(np.full((1, 1, 4), 255, np.uint8)).shape
(1, 1, 3)
```

### an.bench.png.write_png(path, rgb, , level=9)

Write `rgb` as a filter-0 PNG and **verify the round trip** before returning.

The verification is not defensive noise: it is the only thing standing
between a bug in this module’s own encoder and a committed golden that
silently disagrees with the frame it was blessed from. Research §3 asks for
exactly this — “assert the round trip at bless time against the in-memory
screenshot pixels, so a bug in `an`’s own encoder cannot hide”.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)


# _autosummary/an.bench.registry.html.md

# an.bench.registry

The metric declaration table — what each number is, and which way it should move.

This is **data, not prose**, because an#41’s criterion is a query over it:
“>=3 metrics from >=3 distinct causal families, evaluated per mutation, with a
per-metric per-mutation sign declared in advance”. A second, hand-maintained
table of families is how that criterion silently stops being true.

Three things the research requires this table to carry, all three of which
invalidate every prior ledger row if retrofitted:

1. **A per-metric per-mutation direction**, two-sided where the optimum is
   interior. `edge_transition_width` has an interior optimum — under 1 is a
   staircase, 3+ is soft — and the sharpness family moves in *opposite*
   directions for the two mutations, so a single per-metric sign mis-reports
   one of them.
2. **\`\`null\`\` (gated) distinguished from “no change”.** “No change by
   construction” is a tautology and must never count as a satisfied
   prediction, or any pre-encode statistic pads the count for free. Four
   states, not two: `gated` (the reference moved, or the source hash
   differs), `unavailable` (the check could not run — a crashed check is not
   evidence anything is fine), `no_change` (tautology), `not_applicable`
   (wrong side of the encoder).
3. **The family letters**, so the redundancy rule is checkable. Family C
   counts as **one** until the correlation between `coded_luma_edge_error`
   and `chroma_edge_dCr` is measured across the encoder matrix — they were
   r=0.990 in their broken forms and the corrections are *expected* to
   decorrelate them, which is not the same as measured.

The invariants are enforced in `__post_init__`, not by convention.

### Module Attributes

| [`MUTATIONS`](_autosummary/an.bench.registry.html.md#an.bench.registry.MUTATIONS)      | an encoder lever cannot touch a golden-frame metric because the corpus is UPSTREAM of the encoder, so requiring ">=3 metrics" from a CRF change alone would fail because of where the corpus sits, not because the instrument is blind — and that failure would be misdiagnosed as the harness being wrong.   |
|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`REFUSED_LEVERS`](_autosummary/an.bench.registry.html.md#an.bench.registry.REFUSED_LEVERS) | Levers that were TRIED and refused, each with the measured reason — data, so `an bench --mutation <name>` answers with the reason instead of "unknown", and the next person meets the measurement before re-registering one.                                                                                  |
| [`REFERENCE_NOTE`](_autosummary/an.bench.registry.html.md#an.bench.registry.REFERENCE_NOTE) | What an encode-side metric is measured AGAINST.                                                                                                                                                                                                                                                               |
| [`FAMILY_SIDE`](_autosummary/an.bench.registry.html.md#an.bench.registry.FAMILY_SIDE)    | Which side of the encoder each causal family lives on.                                                                                                                                                                                                                                                        |
| [`METRICS`](_autosummary/an.bench.registry.html.md#an.bench.registry.METRICS)        | The panel.                                                                                                                                                                                                                                                                                                    |
| [`TRIPWIRES`](_autosummary/an.bench.registry.html.md#an.bench.registry.TRIPWIRES)      | Tripwires.                                                                                                                                                                                                                                                                                                    |

### Classes

| [`MetricSpec`](_autosummary/an.bench.registry.html.md#an.bench.registry.MetricSpec)(key, family, unit, optimum, ...)     | One row of the panel.                                                            |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`Optimum`](_autosummary/an.bench.registry.html.md#an.bench.registry.Optimum)(kind[, expect, note])                   | Which way "better" points, and whether the optimum is interior.                  |
| [`Prediction`](_autosummary/an.bench.registry.html.md#an.bench.registry.Prediction)(expect[, counts, gate, reason, ...]) | What one metric is expected to do under one mutation, declared in advance.       |
| [`Sweep`](_autosummary/an.bench.registry.html.md#an.bench.registry.Sweep)(grid, shipped)                            | The grid of a threshold counter's OWN free parameters its verdict is checked on. |
| [`Touch`](_autosummary/an.bench.registry.html.md#an.bench.registry.Touch)(path[, differs_only_in])                  | One environment key a lever is expected to change, and HOW.                      |

### Exceptions

| [`RegistryError`](_autosummary/an.bench.registry.html.md#an.bench.registry.RegistryError)   | A metric declaration violates one of the table's invariants.   |
|------------------------------------------------------------------|----------------------------------------------------------------|

### an.bench.registry.FAMILY_SIDE *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'A': 'render', 'B': 'render', 'C': 'encode', 'D': 'encode', 'E': 'encode', 'F': 'encode', 'G': 'encode'}*

Which side of the encoder each causal family lives on. Render-side metrics
are computed on the pre-encode PNG and are blind to the encoder BY
CONSTRUCTION; encode-side metrics compare a decoded frame to its own source
and are structurally blind to render regressions, because the reference
moves with the mutation. They must never be mixed.

### an.bench.registry.METRICS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [MetricSpec](_autosummary/an.bench.registry.html.md#an.bench.registry.MetricSpec)]* *= {'chroma_edge_dCr': MetricSpec(key='chroma_edge_dCr', family='C', unit='code values (8-bit Cr)', optimum=Optimum(kind='one_sided', expect='minimize', note=''), predictions={'high_crf': Prediction(expect='increase', counts=False, gate=None, reason='family C counts as ONE family until the correlation with coded_luma_edge_error is measured across the encoder matrix', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate='reference_moved', reason='measured -0.7% on a faithful AA-off simulation; the claimed +10.7% was refuted. Predict no render-side direction.', reference=None), 'supersample': Prediction(expect=None, counts=False, gate='reference_moved', reason='its subject IS the 4:2:0 subsampling of a conversion whose input this lever changes, so there is no fixed reference. Predict no render-side direction, exactly as for \`disabled_aa\`.', reference=None)}, sentence='How much the colour shifts on the pixels straddling a hard outline once the video is encoded.', role=None, reference='source_png', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'chroma_edge_dCr_over_dY': MetricSpec(key='chroma_edge_dCr_over_dY', family='C', unit='ratio', optimum=Optimum(kind='guard', expect=None, note="3.3 at qp0 yuv420p is real 4:2:0 bleed; 0.96 at crf51 is generic damage wearing the metric's name. The denominator IS \`coded_luma_edge_error\` — the research's \`chroma_edge_dY\` control is mean |dY| over the edge mask, which is that metric's definition verbatim. Measured identical (1.484149 for both on the first real row), so it is recorded once rather than twice: one signal under two names is exactly how a witness count is padded dishonestly."), predictions={'high_crf': Prediction(expect='decrease', counts=False, gate=None, reason='the chroma claim collapses as damage becomes generic', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate='reference_moved', reason='', reference=None), 'supersample': Prediction(expect=None, counts=False, gate='reference_moved', reason='', reference=None)}, sentence='dCr/dY on the edge mask: large means real chroma bleed, ~1 means generic blocking.', role='diagnostic', reference='source_png', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'chroma_edge_dY': MetricSpec(key='chroma_edge_dY', family='C', unit='code values (8-bit Y)', optimum=Optimum(kind='guard', expect=None, note="Read only as the ratio's denominator. NOT a second name for \`coded_luma_edge_error\`: that one references the lossless leg and this one references the PNG conversion, so they differ by exactly the conversion term. They were identical (1.484149) while both referenced the PNG conversion, which is why one of them was removed and then restored when the references diverged."), predictions={'high_crf': Prediction(expect='increase', counts=False, gate=None, reason='control for the ratio below', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate='reference_moved', reason='', reference=None), 'supersample': Prediction(expect=None, counts=False, gate='reference_moved', reason='', reference=None)}, sentence='The luma error on the SAME mask and the SAME reference as \`chroma_edge_dCr\` — a control, not a quality metric.', role='control', reference='source_png', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'coded_luma_edge_error': MetricSpec(key='coded_luma_edge_error', family='C', unit='code values (8-bit Y)', optimum=Optimum(kind='one_sided', expect='minimize', note=''), predictions={'high_crf': Prediction(expect='increase', counts=True, gate=None, reason="family C's witness", reference='0.419 (qp0) -> 1.875 (crf23) -> 41.2 (crf51)'), 'disabled_aa': Prediction(expect=None, counts=False, gate='reference_moved', reason='the source PNG moves with the mutation, so the delta is uninterpretable', reference=None), 'supersample': Prediction(expect=None, counts=False, gate='reference_moved', reason='same gate, same mechanism as \`disabled_aa\`: the source PNG moves with the mutation. A render lever of EITHER sign disqualifies it — and this one is the reason to say so out loud, because a softer source shrinks the edge mask to its easiest members and the number improves MECHANICALLY. Ungated it is one of the 7 unearned improvements a \`mutation=None\` diff reports for this change (an#56).', reference=None)}, sentence="How much the encoder's quantiser roughens the brightness step at a line's edge, measured on the codec's own luma plane.", role=None, reference='lossless', provisional=False, unreviewed=False, tripwire=False, requires='', notes=("Read the coded luma plane; NEVER recompute Y from decoded RGB. The RGB round trip clips at saturated edges, and ~83% of the broken form's baseline was chroma leakage.",), sweep=None), 'edge_masked_distinct_colours': MetricSpec(key='edge_masked_distinct_colours', family='A', unit='count', optimum=Optimum(kind='guard', expect=None, note="A guard, not a dial, for the exact reason Wave 2 deleted the ONE-SIDED \`edge_distinct_colours\`: a 3x3 blur raised that 9.2x and +/-3-LSB noise 55x, and BOTH degradations read as 'AA restored'. What the mask buys is narrower than an#55 assumed, and the difference was MEASURED before this row shipped: an INTERIOR-ONLY change cannot reach the number, but a whole-frame blur can, because the mask is recomputed from the frame being measured and a blur widens the edge band. 3x3 box blur on the six committed goldens, ratio against k=1, whole-frame vs edge-masked: aa_probe 10.25x/9.50x, graded_field 2.04x/2.35x, multi_shot 7.92x/4.63x, promote_demo 1.25x/0.83x, saturated_outline 1.49x/1.14x, single_character 8.60x/5.70x — damped on four of six, WORSE on one, blind on none. So the metric that separates a blur from a supersample is \`edge_transition_width\` (2.1x-3.4x under the same blur against +2.6% to +8.0% under an exact k=2 resolve), and this row is the second half of that reading, not the first. \*\*The pair is evidence for a human reader and NOT a gate\*\* — an#41's criterion counts metrics independently and cannot express a conjunction, so nothing here may be read as one. No figure for THIS metric under a lever exists yet, which is why \`reference\` is unset rather than borrowed from \`frame_distinct_colours\` (an#55)."), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason='pre-encode, and blind to the encoder BY CONSTRUCTION like every family A row: both the mask and the colours come from the source PNG, which sits upstream of the encoder', reference=None), 'disabled_aa': Prediction(expect='decrease', counts=False, gate=None, reason="the same direction as \`frame_distinct_colours\`, deliberately: for flat cutout art essentially all colour variety lives at edges, so the mask is close to a no-op for the COUNT (wave2 research §1.3 measured the two within 6%). The whole-frame count under the lever on \`aa_probe\` goes 7.6 -> 3.0 at k=1 and 21.7 -> 6.9 at k=2 (wave3 research §5) — those are \`frame_distinct_colours\` numbers, NOT this metric's, and \`reference\` is deliberately left unset until a real row measures THIS one. NOT counted: family A spends its one witness on \`edge_transition_width\` and this is the same family. NOT gated either, and the distinction is the one most likely to be flipped by a later reviewer: the mask does move with the lever, but the mask and the number come from the SAME frames, so there is no reference to move. That is what makes \`flat_field_deviation\` further down gated and this one not.", reference=None), 'supersample': Prediction(expect='increase', counts=False, gate=None, reason="MEASURED under the lever before it was declared, on all six scenes, because this metric's whole-frame sibling has a scene-dependent sign and there was no reason to assume the masked one would not. Up on four: \`aa_probe\` +184.6%, \`single_character\` +179.3%, \`multi_shot\` +146.3%, \`graded_field\` +13.6%. Down on the two colour-rich scenes: \`saturated_outline\` -12.0%, \`promote_demo\` -7.6% — where 1x MSAA already emits many one-off blend values and an exact 2x resolve replaces them with fewer, more regular ones, so the picture gets better and the count goes down. \`increase\` is declared because the criterion is evaluated PER SCENE and met on four. NOT counted: family A spends its one witness on \`edge_transition_width\` and this is the same family — read the two together (aa_probe +184.6% colours with +7.4% width is gradation added; the same 3x3 blur that raises this metric 9.5x doubles the width) and the pair separates gradation from softening. It is evidence for a human, never a gate: an#41's criterion counts metrics independently and cannot express a conjunction.", reference='7.58 -> 21.58 on \`aa_probe\`; 422.25 -> 371.75 on \`saturated_outline\`')}, sentence='How many different colours sit ON the edges — the row above, restricted to the edge mask.', role=None, reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=("Its mask is NOT the \`masks.edge\` block the encode-side rows use. That one is ffmpeg's limited-range Y; this one is full-range BT.709 luma from the source RGB, so at the shared threshold of 40 it is the wider mask. The row records it separately as \`masks.render_edge\`, with its own operator.",), sweep=None), 'edge_transition_width': MetricSpec(key='edge_transition_width', family='A', unit='px', optimum=Optimum(kind='interior', expect=None, note='Two-sided: both lost AA and accidental softening (blur, non-integer canvas scale, LINEAR texel filtering) are failures. The absolute value is scene-dependent — a deliberately 3px black outline is legitimately non-flat — so compare deltas on a fixed scene, never absolutes across scenes.'), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason='computed on the pre-encode PNG; the encoder cannot move it', reference=None), 'disabled_aa': Prediction(expect='decrease', counts=True, gate=None, reason="family A's single witness. Holds on five of six corpus scenes; on \`promote_demo\` it moves +0.0001 the OTHER way, because the descriptor path is nearly blind to MSAA (96 differing pixels of 12.4M — an SVG sprite is a pre-rasterised texture, and multisampling applies to WebGL geometry). A \`contrary\` verdict at that magnitude is the lever not reaching the scene, which is why the comparison reports the relative delta beside the direction (an#41).", reference='2.88 -> 2.00 on \`aa_probe\`; 5.6368 -> 5.6369 on \`promote_demo\`'), 'supersample': Prediction(expect='increase', counts=True, gate=None, reason="family A's witness for THIS lever, and scene-dependent by measurement in the exact inverse of \`disabled_aa\`. Holds on the five procedural scenes (\`graded_field\` +8.0%, \`aa_probe\` +7.4%, \`saturated_outline\` +5.2%, \`multi_shot\` +5.1%, \`single_character\` +2.6%) and is \`contrary\` on \`promote_demo\` at -34.8%. That inversion is REAL, not the +0.0001 nothing \`disabled_aa\` produces on the same scene: the SVG sprite rasterises AT 2x instead of being stretched up from a 1x texture, so the descriptor path is the one scene this lever reaches hardest and the AA lever cannot reach at all. The two render levers reach complementary scenes. \`increase\` is declared because the criterion is evaluated PER SCENE and met on at least one — the same shape \`video_stream_bytes\` already carries under \`disabled_aa\`. An increase here is not a regression: the optimum is interior, and this walks TOWARD a measured ceiling (2.5846 \`saturated_outline\`, 2.9167 \`graded_field\`, ~2.452 \`multi_shot\`, ~2.140 \`single_character\`, ~3.503 \`promote_demo\`). \`aa_probe\` has NO ceiling — its diagonals land the block-mean grid differently at every k, so it oscillates +/-5-8% with no settling — and gets no declared target, because a value nobody measured is not a value (research §3a).", reference='2.3685 -> 2.4921 on \`saturated_outline\`; 5.6368 -> 3.6775 on \`promote_demo\`')}, sentence='The average thickness, in pixels, of the fuzzy band between two flat colour areas — under 1 is a jagged staircase, ~1 is clean AA, 3+ means the picture has gone soft.', role=None, reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'encode_flicker_on_held_pixels': MetricSpec(key='encode_flicker_on_held_pixels', family='E', unit='fraction of held px moving >=2', optimum=Optimum(kind='one_sided', expect='minimize', note=''), predictions={'high_crf': Prediction(expect='increase', counts=True, gate=None, reason="family E's witness — and the least reliable of the four. It is NON-MONOTONE across the CRF ladder on the real corpus (0.000648 / 0.007018 / 0.000985 / 0.000916 / 0.001137 / 0.001685 over crf 18/23/28/33/40/51 on \`single_character\`, peaking at crf23), because at high CRF the whole frame flattens into large uniform skip regions and held pixels stop moving — the same mechanism the \`disabled_aa\` gate below documents. At the crf23 -> crf40 step the lever uses it holds on five of six scenes and inverts on \`single_character\`, so it is kept and not leaned on: C, D and F are monotone across the whole ladder and satisfy the criterion on all six scenes without it (an#41).", reference='0.0113 -> 0.0243 on aa_probe; 0.0070 -> 0.0011 on single_character'), 'disabled_aa': Prediction(expect=None, counts=False, gate='source_hash_differs', reason="excluded from EVERY renderer mutation's witness count: without the source gate, half-res-then-nearest-upscale — the most visible possible flat-art regression — reports a 7.1x IMPROVEMENT, because a flattened render gives x264 large uniform skip regions.", reference=None), 'supersample': Prediction(expect=None, counts=False, gate='source_hash_differs', reason="'excluded from EVERY renderer mutation's witness count' is what the declaration beside \`disabled_aa\` says, and this is the second renderer mutation it was written for. Worth restating because THIS lever is the exact shape of the failure that gate exists for: without it, half-res-then-nearest-upscale — the most visible possible flat-art regression — reports a 7.1x improvement, because a flattened render gives x264 large uniform skip regions. A block-mean resolve flattens the source in the same direction, so ungated this metric would reward the improvement for the identical wrong reason and nobody could tell the two apart.", reference=None)}, sentence='The fraction of pixels the animator held perfectly still that moved by at least 2 code values in the delivered video.', role=None, reference='lossless', provisional=False, unreviewed=False, tripwire=False, requires='', notes=("The panel's other hard-threshold counter, so its row carries the count at every tol of \`metrics.FLICKER_DELTA_TOL_SWEEP\` and \`an bench-compare\` reports a direction that grid disagrees with as \`unstable\` (an#140). Measured: under \`high_crf\` \`graded_field\` reads -9% at tol 1 and +29% at tol 2; under 4:2:0 -> 4:4:4 \`single_character\` reads +25% at tol 2 and -15% at tol 3.",), sweep=Sweep(grid=(('tol', (1, 2, 3, 4)),), shipped=(('tol', 2),))), 'encode_ringing_excess': MetricSpec(key='encode_ringing_excess', family='G', unit='code values', optimum=Optimum(kind='one_sided', expect='minimize', note=''), predictions={'high_crf': Prediction(expect='increase', counts=False, gate=None, reason='provisional; kept out of the witness count until open question 4 is settled', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate='source_hash_differs', reason="declared 'both legs rise together by construction'; measured moving on ALL SIX corpus scenes under the real MSAA lever (an#41). The cancellation is exact only when both legs share a FIXED source, and a renderer mutation moves the source — so what is left is uninterpretable rather than zero.", reference=None), 'supersample': Prediction(expect=None, counts=False, gate='source_hash_differs', reason='the cancellation is exact only when both legs share a FIXED source; this lever moves it. Measured moving on all six scenes under \`disabled_aa\`, and nothing about the sign of the render change makes it hold still.', reference=None)}, sentence='How much more the encoder overshoots around outlines than a mathematically lossless encode of the same frames does.', role=None, reference='source_png', provisional=True, unreviewed=False, tripwire=False, requires='', notes=('Provisional pending a cheap comparison against plain edge-band MAE over the identical mask. \`edge_band_mae\` is recorded beside it so the first ledger row answers that question.',), sweep=None), 'expression_min_pairwise_changed_px': MetricSpec(key='expression_min_pairwise_changed_px', family='B', unit='pixels', optimum=Optimum(kind='guard', expect=None, note="Not a quality dial: larger is not better beyond 'the presets are apart'. It exists so a regression that makes two expressions render alike moves a ledger number instead of waiting for someone to look."), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason='computed on the pre-encode PNGs; no encode change can reach it', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate="AA-off moves every edge on every pinned frame, so the count of differing pixels between two frames moves with it in an undeclared direction — measured on the lane: it MOVED under \`supersample\` when first declared \`not_applicable\`. Gated, not predicted: the delta is uninterpretable, not good or bad. Family B's witness is \`min_ssim_win8_vs_golden\`.", reason='', reference=None), 'supersample': Prediction(expect=None, counts=False, gate='same as disabled_aa: an edge-quality lever softens every edge on every pinned frame and the pairwise count moves with it; not a face-solver lever, no declared sign', reason='', reference=None)}, sentence="The smallest number of pixels by which any two of the scene's pinned frames differ in TODAY'S render — on \`expressions\`, the closest pair of presets; a collapse of two emotions onto one face drives it to zero (an#98). On a two-frame scene it is that pair's own change, measured rather than withheld: a render-side row is never null on a real capture.", role='diagnostic', reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=('Measured at the first bless (an#98): 106 px between \`thinking\` and \`skeptical\`, the two asymmetric presets, and 384 px at the far end. \`tests/test_expression_goldens.py\` pins half the minimum on the COMMITTED goldens; this row reports the same quantity on the live render, so the ledger sees it before a re-bless does.',), sweep=None), 'file_bytes': MetricSpec(key='file_bytes', family='F', unit='bytes', optimum=Optimum(kind='guard', expect=None, note='Contaminated by the audio cache: the renderer always emits an AAC track, silent or not. Prefer video_stream_bytes.'), predictions={'high_crf': Prediction(expect='decrease', counts=False, gate=None, reason='', reference=None), 'disabled_aa': Prediction(expect='increase', counts=False, gate=None, reason='companion to \`video_stream_bytes\`, SCENE-DEPENDENT in the same way, and additionally contaminated by the audio track. Measured AA-on -> off across the corpus: +3.6% / -2.7% / +7.0% / -0.0% / +4.0% / -2.9%, so it is \`contrary\` on half of it. Counts nothing; kept only as the companion the sentence above calls it (an#41 review).', reference=None), 'supersample': Prediction(expect='decrease', counts=False, gate=None, reason='companion to \`video_stream_bytes\`, contaminated by the AAC track the renderer always emits, silent or not, and therefore scene-dependent in the same way and then some. MEASURED alongside it rather than assumed to follow: it does follow, on every scene and with the same three-three split — \`single_character\` -5.9%, \`multi_shot\` -4.8%, \`saturated_outline\` -2.4%; up and small on \`promote_demo\` +0.6%, \`aa_probe\` +0.5%, \`graded_field\` +0.4%. Counts nothing — prefer \`video_stream_bytes\`, as its own \`optimum\` note says.', reference=None)}, sentence='The whole mp4 on disk, audio track included.', role='companion', reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'flat_field_deviation': MetricSpec(key='flat_field_deviation', family='D', unit='fraction of flat px with |d|>6', optimum=Optimum(kind='one_sided', expect='minimize', note=''), predictions={'high_crf': Prediction(expect='increase', counts=True, gate=None, reason="family D's witness; monotone over a 133x span", reference='0.0003 / 0.0005 / 0.0035 / 0.0127 / 0.0399 (crf18->51)'), 'disabled_aa': Prediction(expect=None, counts=False, gate='source_hash_differs', reason="The research measured this flat across a SIMULATED AA matrix and called the orthogonality 'the metric's whole value'. Run against the real MSAA lever it moves on ALL SIX corpus scenes, in both directions (an#41). The mechanism is structural rather than surprising: the flat mask is derived from the SOURCE frames, which this lever changes, so the mask itself moves and the comparison has no fixed reference. That is the definition of gated — uninterpretable, not good or bad — and it is the same gate \`encode_flicker_on_held_pixels\` already carries for every renderer mutation, for the same reason.", reference=None), 'supersample': Prediction(expect=None, counts=False, gate='source_hash_differs', reason='the flat mask is derived from the SOURCE frames, which this lever changes, so the mask itself moves and the comparison has no fixed reference — structurally identical to \`disabled_aa\`, and the DIRECTION of the render change is irrelevant to it. Note the gate is \`source_hash_differs\` and NOT \`reference_moved\`: an#56 describes every C/D/E/G row as \`reference_moved\`, which is right for 5 of the 9 and wrong for this one, \`flat_field_p99_dev\`, \`encode_flicker_on_held_pixels\` and \`encode_ringing_excess\`. The two gates are recorded separately on purpose: one says the reference moved, this one says the MASK moved.', reference=None)}, sentence='Of the pixels the renderer painted inside a large flat colour field, what fraction came back more than 6 code values off.', role=None, reference='lossless', provisional=False, unreviewed=False, tripwire=False, requires='', notes=('Covers the ~90% of the frame no edge metric touches. Banding regressions are invisible without it.', 'A hard-threshold count over a mask eroded by a second free parameter, so its row carries the count at every (dilate_k, tol) of \`metrics.FLAT_DILATE_K_SWEEP\` x \`FLAT_DEV_TOL_SWEEP\`, and \`an bench-compare\` reports a direction the grid disagrees with as \`unstable\` — neither counted nor contrary (an#140). Measured under 4:2:0 -> 4:4:4: 3 of 6 scenes reverse on that grid (\`graded_field\` +84.2% at tol 6, -81.6% at tol 8), and so does \`graded_field\` under \`high_crf\` (tol 5). No scalar replacement was stable on the same grid — mean excess over tol, the threshold-free mean, p99 and p99.9 each reverse on at least one scene — because the two deviation distributions CROSS (16 of 18 scene x k cells): some deviations shrink and others grow, and any scalar then encodes a choice of weighting as a direction.'), sweep=Sweep(grid=(('dilate_k', (1, 3, 5)), ('tol', (4, 5, 6, 7, 8))), shipped=(('dilate_k', 3), ('tol', 6)))), 'flat_field_p99_dev': MetricSpec(key='flat_field_p99_dev', family='D', unit='code values', optimum=Optimum(kind='one_sided', expect='minimize', note=''), predictions={'high_crf': Prediction(expect='increase', counts=False, gate=None, reason='companion to the rate above', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate='source_hash_differs', reason='gated for the same structural reason as the rate above: the flat mask moves with the source. Measured moving on one of six scenes and holding on five, which is what a metric with no fixed reference looks like — not evidence of orthogonality (an#41).', reference=None), 'supersample': Prediction(expect=None, counts=False, gate='source_hash_differs', reason='gated for the same structural reason as the rate above: the flat mask moves with the source, under a render lever of either sign.', reference=None)}, sentence='The 99th percentile of the same deviation, in human units.', role='companion', reference='lossless', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'frame_distinct_colours': MetricSpec(key='frame_distinct_colours', family='A', unit='count', optimum=Optimum(kind='guard', expect=None, note='No predicted direction on AA changes. A guard, not a dial.'), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason='pre-encode', reference=None), 'disabled_aa': Prediction(expect='decrease', counts=False, gate=None, reason='do not count it alongside off_palette_pixel_fraction; same family', reference=None), 'supersample': Prediction(expect='increase', counts=False, gate=None, reason="do not count it alongside \`edge_transition_width\`; same family. SCENE-DEPENDENT and measured (research §4): +197% \`single_character\`, +186% \`aa_probe\`, +184% \`multi_shot\`, +10% \`graded_field\` — and DOWN 18% on \`saturated_outline\` and 23% on \`promote_demo\`. On a scene that already carries a lot of colour, 1x MSAA emits many one-off blend values and an exact 2x resolve replaces them with fewer, more regular ones: the picture gets BETTER and the count goes DOWN. This is what refutes epic #9's 'edge distinct-colour count materially up on every corpus scene' — the statistic has a scene-dependent sign, which is not a magnitude problem and cannot be fixed by rendering harder. (The metric that done-when literally names, \`edge_distinct_colours\`, does not exist: refuted and deleted in Wave 2.) Read as a PAIR with \`edge_transition_width\` — colours up + width up slightly = gradation added; colours +3600% + width +45% = lanczos ringing; colours down + width down = sharpened. an#41's criterion counts metrics INDEPENDENTLY and cannot express that conjunction, so the pair is evidence for a human reader and is NOT a gate.", reference='7.6 -> 21.7 on \`aa_probe\`; 478.2 -> 392.3 on \`saturated_outline\`')}, sentence='How many different colours are in the frame at all.', role=None, reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'min_ssim_win8_vs_golden': MetricSpec(key='min_ssim_win8_vs_golden', family='B', unit='ssim', optimum=Optimum(kind='one_sided', expect='maximize', note='SIGN AMBIGUITY, recorded deliberately: a supersampling IMPROVEMENT moves this away from 1.0 exactly as an AA regression does, and in changed-pixel terms reports the improvement as the LARGER change. It is a change detector, and must be paired with the no-reference family to say whether a change was good.'), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason='the golden corpus is UPSTREAM of the encoder; no encode change can reach it', reference=None), 'disabled_aa': Prediction(expect='decrease', counts=True, gate=None, reason="family B's single witness. \`golden_identity\` is the tripwire beside it and counts ZERO — one family, at most one witness, and a boolean change detector is not it.", reference='0.9999 -> 0.279 at 1080p / 0.063 at native for a total eye-blink'), 'supersample': Prediction(expect='decrease', counts=True, gate=None, reason="family B's single witness, and THIS lever is what the SIGN AMBIGUITY note above was written about: a supersampling IMPROVEMENT moves this away from 1.0 exactly as an AA regression does, and reports the improvement as the LARGER change. It counts as evidence the render CHANGED, which is what family B measures; it is NOT evidence the change was good, and the note beside \`optimum\` is the standing disclaimer that must be read with it. Fires on all six scenes: the committed baseline is 1.0 on every one, and research §2/§4 measured a real pixel move on every one — including \`promote_demo\`, which \`disabled_aa\` cannot reach at all. The magnitude is the lever's own, from the exam run recorded in the an#56 PR body — not a figure lifted from a research table, which is the an#41-review defect this panel already caught once.", reference="1.0 -> 0.4380 on \`single_character\`, 0.4449 on \`promote_demo\`, 0.6144 on \`saturated_outline\`, 0.7756 on \`aa_probe\`, 0.7969 on \`multi_shot\`, 0.8201 on \`graded_field\` — every scene, because every scene's pixels move")}, sentence='The worst small window in the frame, scored against the committed golden — magnitude AND location of a golden failure.', role='diagnostic', reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=('The metrics survey concluded SSIM should be excluded because whole-frame SSIM scores a total eye-blink at 0.9989. That was REFUTED: only the global-moment reduction is blind. Killing SSIM outright would have discarded the best numpy-only detector available.',), sweep=None), 'off_palette_pixel_fraction': MetricSpec(key='off_palette_pixel_fraction', family='A', unit='fraction', optimum=Optimum(kind='one_sided', expect='minimize', note='Honest caveat: a blur, drop shadow, gradient or sub-pixel offset moves this UP, indistinguishably from better AA. It is a change detector on that axis. Its floor is scene-dependent and is NOT zero on the SVG-sprite path.'), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason="pre-encode. NOT MEASURED — which is not the same as 'no change'", reference=None), 'disabled_aa': Prediction(expect='decrease', counts=False, gate=None, reason='family A already supplies edge_transition_width as its witness', reference=None), 'supersample': Prediction(expect='increase', counts=False, gate=None, reason="family A already supplies \`edge_transition_width\` as its witness — count at most one per family. \`increase\` and NOT \`not_applicable\`: this is a render lever and the metric is render-side, so \`not_applicable\` would make every scene report \`unexpected_movement\` for a metric doing exactly what it should. An exact block-mean resolve replaces hard edge pixels with blends, and a blend is off-palette by definition. MEASURED on the corpus and scene-dependent, like everything else this lever touches: \`single_character\` +28.9%, \`multi_shot\` +24.2%, \`aa_probe\` +21.8%, \`graded_field\` +0.0%; contrary on the two scenes whose k=1 frames are already the most off-palette, \`promote_demo\` -22.8% and \`saturated_outline\` -9.9%, where an exact resolve REPLACES many one-off MSAA blends with fewer, more regular ones. It counts nothing either way. And the optimum says \`minimize\` while this improvement moves it UP, which is the metric's own caveat stated in its \`note\`: on this axis it is a change detector, not a quality dial.", reference=None)}, sentence='The fraction of the frame whose pixels are not exactly one of the colours the compiler declared for this shot.', role=None, reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'ring_band_mae': MetricSpec(key='ring_band_mae', family='G', unit='code values', optimum=Optimum(kind='one_sided', expect='minimize', note="Research §1.8 calls this 'plain edge-band MAE over the identical mask'. Named for the mask it actually uses, because MAE over the EDGE mask is \`coded_luma_edge_error\`'s definition verbatim — measured identical on the first real row — and recording it twice would answer open question 4 with a tautology."), predictions={'high_crf': Prediction(expect='increase', counts=False, gate=None, reason='the comparison arm', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate='reference_moved', reason='', reference=None), 'supersample': Prediction(expect=None, counts=False, gate='reference_moved', reason='', reference=None)}, sentence='Plain mean absolute luma error over the RING band — the simpler rival to \`encode_ringing_excess\`, on the same mask and with no second encode.', role='q4_comparator', reference='source_png', provisional=False, unreviewed=False, tripwire=False, requires='', notes=(), sweep=None), 'stage_min_plane_ratio_gap': MetricSpec(key='stage_min_plane_ratio_gap', family='B', unit='ratio', optimum=Optimum(kind='guard', expect=None, note='Not a quality dial: a larger gap is not a better picture, it is a fixture whose depths are further apart. It exists so a regression that flattens the parallax moves a ledger number instead of waiting for someone to look at a GIF.'), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason='computed on the pre-encode PNGs; no encode change can reach it', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate="AA-off changes which pixels carry a plane's exact colour along its edges, so a centroid can shift by a fraction of a pixel and the mask COUNT can change — which the measurement refuses outright as a clipped plane. Gated, not predicted: the outcome is 'the instrument declines', which is neither better nor worse.", reason='', reference=None), 'supersample': Prediction(expect=None, counts=False, gate="same as disabled_aa: an edge-quality lever moves the exact-colour mask's boundary, and this measurement is defined on exact colours", reason='', reference=None)}, sentence="The smallest gap between any two planes' displacement RATIOS on a panning multiplane stage (an#111). Each plane's centroid displacement is divided by the reference plane's, so a stage that parallaxes gives the declared depths back — on \`stage_pan\`, 0.25 / 1.0 / 2.0 — and a stage that flattened gives every plane 1.0 and this row zero. \`unavailable\`, never zero, for a scene with fewer than two colour-filled planes: a scene with nothing to compare has no ratio, and reporting that as flattened would fire on every other fixture.", role='diagnostic', reference='none', provisional=False, unreviewed=False, tripwire=False, requires='a multiplane stage: two or more colour-filled planes', notes=("The trap this metric is shaped around: today's centre-anchored zoom ALREADY gives unequal per-plane displacements, so 'the planes moved at different rates' is satisfied by a scene with no parallax at all. The JSON half of the measurement probes at scene-space x = 0, where the zoom term cancels exactly; the pixel half cannot (a centroid sits at the plane's own offset), so the \`stage_pan\` fixture holds zoom CONSTANT instead.", 'Ratios are taken against the LARGEST mover, always. The pixel half cannot see a \`depth\`, so a depth-aware reference makes the two halves report different numbers for one stage — measured while this landed: 0.75 against the \`depth == 1\` plane and 0.375 against the largest mover, for the same measurement.', "Measured at the first bless: 0.375, the far/mid gap on depths 0.25 / 1.0 / 2.0 (reported as ratios 0.125 / 0.5 / 1.0). The \`stage_planes_parallaxed\` tripwire's floor is half of it, which is the \`expression_min_pairwise_changed_px\` precedent followed literally."), sweep=None), 'stage_planes_parallaxed': MetricSpec(key='stage_planes_parallaxed', family='B', unit='boolean', optimum=Optimum(kind='guard', expect=None, note='A change detector. Counts zero toward any criterion.'), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason='computed on the pre-encode PNGs', reference=None), 'disabled_aa': Prediction(expect=None, counts=False, gate='see stage_min_plane_ratio_gap', reason='', reference=None), 'supersample': Prediction(expect=None, counts=False, gate='see stage_min_plane_ratio_gap', reason='', reference=None)}, sentence='True when the planes moved at DIFFERENT rates — i.e. the stage parallaxed rather than panning as one rigid image. The same measurement as \`stage_min_plane_ratio_gap\`, read as a verdict: a boolean and the number beside it must be the same evidence, or a reader has to reconcile them.', role='tripwire', reference='none', provisional=False, unreviewed=False, tripwire=False, requires='a multiplane stage: two or more colour-filled planes', notes=("There is deliberately NO \`flat_camera\` mutation lever to prove this fires. A compile-time parallax change moves the scene's contract hash, so \`bench-compare\` refuses the row at comparability before any family is examined — the recorded \`step_hz\` verdict, verbatim. The proof belongs in \`an bench-mutants\` as a declared guard mutant.",), sweep=None), 'video_stream_bytes': MetricSpec(key='video_stream_bytes', family='F', unit='bytes', optimum=Optimum(kind='guard', expect=None, note='Not a quality dial in either direction — a cross-check that the metrics read the file they think they do.'), predictions={'high_crf': Prediction(expect='decrease', counts=True, gate=None, reason='free fourth cross-check', reference='x1.18 to x1.77 at crf23 -> crf40; x2.07 across the whole ladder'), 'disabled_aa': Prediction(expect='increase', counts=True, gate=None, reason="hard edges cost more to code. THE WEAKEST of this mutation's three witnesses, the only unreviewed metric in the set — and SCENE-DEPENDENT, measured. It holds where the lever has non-axis-aligned edges to change (aa_probe +6.6%, multi_shot +9.7%, saturated_outline +6.9%) and INVERTS where it does not (single_character -6.1%, graded_field -5.7%, promote_demo -0.1%), because AA-off on axis-aligned art removes intermediate colours and the picture gets CHEAPER instead of harder. So this mutation's criterion is met per scene, on the scenes the lever can reach — which is the measured reason \`aa_probe\` is in the corpus at all (an#38, an#41).", reference='+6.6% / -6.1%, scene-dependent'), 'supersample': Prediction(expect='decrease', counts=True, gate=None, reason="family F's witness, and the ONLY third family available to a render lever: A and B can count, C/D/E/G are all gated because their masks and references derive from the source frames, and F is the one encode-side family whose reference is \`none\` — a property of the encoded file rather than a comparison against a moving source. So an#41's three-family criterion for this lever is A + B + F, forced, with no substitute. MEASURED before it was declared, because two mechanisms fight here and guessing was not available: a block-mean resolve ADDS distinct values at edges (+184-197% on the three colour-poor scenes) and it LOWERS edge frequency, which is cheaper for the DCT. The second wins where there is geometry to smooth. Scene-dependent, the same shape this row already carries under \`disabled_aa\` — and \`decrease\` is declared because that is the sign that holds on the scenes where family A also holds, and because it is the side carrying the magnitude: the three down-moves are -4.0% to -12.2% and the three up-moves are +0.8% to +2.8%.", reference='single_character -12.2%, multi_shot -6.6%, saturated_outline -4.0%; contrary and small on graded_field +2.8%, promote_demo +1.1%, aa_probe +0.8%')}, sentence='How large the encoded video stream is, with the audio track excluded.', role=None, reference='none', provisional=False, unreviewed=True, tripwire=False, requires='', notes=('The one metric in the panel that went through NO adversarial pass.',), sweep=None)}*

The panel. Order is display order; the ledger sorts keys anyway.

### an.bench.registry.MUTATIONS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('high_crf', 'disabled_aa', 'supersample')*

an
encoder lever cannot touch a golden-frame metric because the corpus is
UPSTREAM of the encoder, so requiring “>=3 metrics” from a CRF change alone
would fail because of where the corpus sits, not because the instrument is
blind — and that failure would be misdiagnosed as the harness being wrong.

`supersample` is the third and it is the odd one out on purpose: the other
two make the picture worse and it makes the picture BETTER. A panel that has
only ever been shown degradations cannot tell an improvement from a
regression — run as a plain commit-to-commit diff this change reports 2
false regressions and 7 unearned improvements (an#56). Declaring it is how
that gets found before it is believed.

* **Type:**
  The levers an#41 and an#56 use. At least one per SIDE is mandatory

### *class* an.bench.registry.MetricSpec(key, family, unit, optimum, predictions, sentence, role=None, reference='none', provisional=False, unreviewed=False, tripwire=False, requires='', notes=<factory>, sweep=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One row of the panel.

#### requires *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

What a scene must HAVE for this row to exist at all, or `""` when the
row applies to every scene (an#111).

The render-side panel rule is “nothing may be null on a real capture,
because a null render-side row is a blind panel”. That rule assumes
every metric could have been measured. `stage_min_plane_ratio_gap`
could not: a displacement ratio needs two planes moving at different
depths, and `single_character` has no planes at all — so its null is
structural, not a gap in the instrument.

Declared rather than hardcoded in the test, so the panel rule keeps
naming its own exceptions instead of a test file carrying a list the
registry does not know about.

#### sweep *: [Sweep](_autosummary/an.bench.registry.html.md#an.bench.registry.Sweep) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Set only on a hard-threshold counter (an#140); see [`Sweep`](_autosummary/an.bench.registry.html.md#an.bench.registry.Sweep).

### *class* an.bench.registry.Optimum(kind, expect=None, note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Which way “better” points, and whether the optimum is interior.

### *class* an.bench.registry.Prediction(expect, counts=False, gate=None, reason='', reference=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What one metric is expected to do under one mutation, declared in advance.

`expect=None` means **gated**: the number is uninterpretable rather than
good or bad, and `gate` says why. It is not “no change”.

### an.bench.registry.REFERENCE_NOTE *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'lossless': 'the decode of a -qp 0 encode — the plane libx264 received', 'none': 'a property of the encoded file, not a comparison', 'source_png': 'an explicit RGB->YUV conversion of the pre-encode PNGs'}*

What an encode-side metric is measured AGAINST. Two answers, and the choice
is per metric rather than global:

- `lossless` — the decode of a `-qp 0` encode of the same frames. That IS
  the plane libx264 received, on any build, so the metric is pure quantiser
  damage with no colour-conversion term. The default, and what every
  counting encode-side metric uses.
- `source_png` — an explicit RGB->YUV conversion of the pre-encode PNGs.
  Required by exactly two things. The chroma metric’s subject IS the 4:2:0
  subsampling that happens during that conversion – the term the lossless
  leg cancels, so a lossless-referenced version is blind to it (it does NOT
  read ~0; measured, it reads 1.7-2.5 against this metric’s 2.9-9.3, because
  it measures chroma quantiser damage instead – an#72). And `encode_ringing_excess`
  cancels a term that only exists when BOTH its legs share this reference —
  against the lossless leg it degenerates to raw overshoot, which is the
  refuted form.

The distinction became visible when CI measured the PNG conversion to be
build-dependent (exact on ffmpeg 8.1, mean 0.63 / max 5 on the Linux
runner’s older build). It is recorded per metric so a reader can tell which
numbers carry that term.

### an.bench.registry.REFUSED_LEVERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'flat_camera': 'a compile-time change to the plane-compensation channels, refused at comparability for the same reason as \`step_hz\`; the parallax claim is carried render-side by \`stage_min_plane_ratio_gap\` (an#111).', 'pix_fmt': "4:2:0 -> 4:4:4 at fixed CRF fails its own exam on the machine it was designed on, not only on Linux (an#72). Re-measured 2026-09-30 on macOS/arm64, ffmpeg 9.0.1, lossless leg tracking the delivered format (an#138) and with an#140's robustness gate: the three-family criterion is met on 1 of 10 scenes (\`dialogue\`); the pre-an#138 'four scenes' was taken against a contaminated reference. Three reasons, each measured: (1) the lever is not one variable — at fixed CRF 4:4:4 encodes at a coarser quantiser (I-frame Avg QP +2.9 to +9.5) and spends fewer bits, so every encode-side direction is the sum of two opposed effects; (2) family D cannot carry it — the flat-field deviation distributions CROSS (16 of 18 scene x dilate cells on the six lever scenes), so its direction is \`unstable\` over its own tol/dilate grid on 5 of 10 scenes; (3) family E's held mask is derived from the lossless leg, which moves with the format (0.02-2.65% of held pixels), so E has no fixed mask under this lever. The one witness whose subject IS chroma, \`chroma_edge_dCr\`, references \`source_png\` and may not count. The knob ships (\`an render --pix-fmt\`); a QUANTISER-matched lever is the route the evidence leaves open.", 'step_hz': 'moves \`scene_contract_sha256\` on every scene with a tween, so \`bench-compare\` refuses the row before any family is examined; the instrument is per-frame and stepping is a temporal choice (an#89).'}*

Levers that were TRIED and refused, each with the measured reason — data, so
`an bench --mutation <name>` answers with the reason instead of “unknown”,
and the next person meets the measurement before re-registering one. Never
pulled, never predicted for; disjoint from `MUTATIONS` by test.

### *exception* an.bench.registry.RegistryError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A metric declaration violates one of the table’s invariants.

### *class* an.bench.registry.Sweep(grid, shipped)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The grid of a threshold counter’s OWN free parameters its verdict is checked on.

Declared rather than merely written by `run.py`, so a row says a metric
SHOULD carry a sweep: a sweep deleted from a row is then refused by
`an bench-compare` instead of silently reading as “never had one” (an#140).
`shipped` names the cell that IS the metric’s value.

### an.bench.registry.TRIPWIRES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [MetricSpec](_autosummary/an.bench.registry.html.md#an.bench.registry.MetricSpec)]* *= {'golden_identity': MetricSpec(key='golden_identity', family='B', unit='boolean', optimum=Optimum(kind='guard', expect=None, note='Full-frame, never edge-masked.'), predictions={'high_crf': Prediction(expect='not_applicable', counts=False, gate=None, reason='the corpus is UPSTREAM of the encoder; no encode change can reach it', reference=None), 'disabled_aa': Prediction(expect='decrease', counts=False, gate=None, reason='it FAILS — a change detector firing, not a quality measurement, which is why it counts ZERO. Spelled \`decrease\` (True -> False) rather than \`no_change\`: the prediction was \`no_change\` while this very sentence said it fails, so the row reported \`unexpected_movement\` on every scene for a tripwire doing exactly its job. Found by \`an bench-compare\` (an#41).', reference=None), 'supersample': Prediction(expect='decrease', counts=False, gate=None, reason='it FAILS — \`True -> False\` — on all six scenes, because a block-mean resolve changes pixels on all six and the committed baseline is \`True\` on all six. A change detector firing, not a quality measurement, which is why it counts ZERO — and under THIS lever that distinction is the whole point: the tripwire fires identically for an improvement and for a regression, so it can never be the evidence that a supersample was worth shipping. Spelled \`decrease\` rather than \`no_change\` for the reason recorded beside \`disabled_aa\`.', reference=None)}, sentence="Today's frame is pixel-for-pixel the committed golden frame.", role=None, reference='none', provisional=False, unreviewed=False, tripwire=True, requires='', notes=('Compare sha256 of DECODED pixels, never file bytes: Chromium 1187 -> 1223 changes 144/144 PNG files and zero pixels.',), sweep=None)}*

Tripwires. A separate block from `METRICS` because they count ZERO toward
any criterion — they fire on improvements and regressions alike.

### *class* an.bench.registry.Touch(path, differs_only_in=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One environment key a lever is expected to change, and HOW.

`differs_only_in` names the argv flags whose values the lever may move.
Without it the exemption is by PATH, and `x264_argv` is the whole encode
command — so a `-preset medium` -> `-preset veryslow` change, which moves
every encode-side number, rode in on the CRF lever’s exemption and was
reported as “the lever moved it — expected” (an#41 review). `None` means
any difference is the lever’s, which is right for an opaque digest.

#### is_the_levers_change(before, after)

True when the observed difference is the one this lever makes.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)


# _autosummary/an.bench.run.html.md

# an.bench.run

`run_bench`: render the corpus, compute the panel, write one ledger row.

\*\*Every encode-side metric is measured against a lossless (`-qp 0`) encode of
the same frames, not against a second conversion of the PNGs.\*\* That is a
correction, and CI is what made it: the PNG conversion agrees with the
encoder’s own exactly on ffmpeg 8.1 and disagrees by mean 0.63 / max 5 on the
Linux runner’s older build — 42% of `coded_luma_edge_error`’s whole crf23 value,
which would have been measured as encoder damage on that machine and as nothing
on this one. `-qp 0` is lossless, so its decoded luma **is** the plane libx264
received, on any build. Referencing to it removes the assumption rather than
widening it. See [`an.bench.imageio`](_autosummary/an.bench.imageio.html.md#module-an.bench.imageio).

The PNG conversion is still performed and its distance from the encoder’s input
is recorded as `png_to_encoder_input_luma` — that number is the build
dependence, and it belongs in provenance rather than inside a gate.

### Module Attributes

| [`GOLDEN_METRIC_KEY`](_autosummary/an.bench.run.html.md#an.bench.run.GOLDEN_METRIC_KEY)              | Family B's two rows, one in each block.                                                                                                                                                         |
|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`MIN_PINNED_FRAMES_FOR_PAIRWISE`](_autosummary/an.bench.run.html.md#an.bench.run.MIN_PINNED_FRAMES_FOR_PAIRWISE) | A pairwise minimum needs two frames; every fixture pins at least two, so on a real capture this row is always measured (the render-side panel may not be null — `tests/test_bench_capture.py`). |
| [`STAGE_METRIC_KEY`](_autosummary/an.bench.run.html.md#an.bench.run.STAGE_METRIC_KEY)               | The pan measurement (an#111).                                                                                                                                                                   |
| [`STAGE_MIN_RATIO_GAP`](_autosummary/an.bench.run.html.md#an.bench.run.STAGE_MIN_RATIO_GAP)            | The floor the tripwire fires below, set at HALF the first bless's measured minimum — the `expression_min_pairwise_changed_px` precedent followed literally.                                     |
| [`JUST_BLESSED_DETAIL`](_autosummary/an.bench.run.html.md#an.bench.run.JUST_BLESSED_DETAIL)            | Said when a run blessed the goldens it would otherwise have compared against.                                                                                                                   |

### Functions

| [`conversion_distance`](_autosummary/an.bench.run.html.md#an.bench.run.conversion_distance)(frames_dir, ...)              | How far this build's PNG->YUV conversion sits from the encoder's own.                                                                                                             |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`format_panel`](_autosummary/an.bench.run.html.md#an.bench.run.format_panel)(ledger)                              | A human-readable digest of a row — the thing `an bench` prints.                                                                                                                   |
| [`lossless_reference`](_autosummary/an.bench.run.html.md#an.bench.run.lossless_reference)(frames_dir, fps, out, \*, ...) | Encode the frames losslessly and return the decode — the encoder's input.                                                                                                         |
| [`naming_git_state`](_autosummary/an.bench.run.html.md#an.bench.run.naming_git_state)(git, \*, blessed, root)          | Which git state NAMES the row: the tree the run **left**, not the one it read.                                                                                                    |
| [`pinned_frames_min_pairwise_changed_px`](_autosummary/an.bench.run.html.md#an.bench.run.pinned_frames_min_pairwise_changed_px)(...)        | `expression_min_pairwise_changed_px` for one scene: decode the pinned frames from today's render and take the minimum over every pair of the count of pixels that differ (an#98). |
| [`run_bench`](_autosummary/an.bench.run.html.md#an.bench.run.run_bench)(\*[, scenes, out, keep_render, ...])    | Render the corpus, compute the panel, and (by default) write the row.                                                                                                             |
| [`shot_policy_provenance`](_autosummary/an.bench.run.html.md#an.bench.run.shot_policy_provenance)(shots)                     | The per-shot COMPILE POLICIES a scene rendered under, keyed by shot id.                                                                                                           |
| [`viseme_keyframes_per_second`](_autosummary/an.bench.run.html.md#an.bench.run.viseme_keyframes_per_second)(scene_json)           | Viseme keyframes per second of dialogue in a compiled shot, or `None` when nothing speaks (an#97).                                                                                |

### Exceptions

| [`BenchError`](_autosummary/an.bench.run.html.md#an.bench.run.BenchError)   | The bench could not produce a row it would be honest to file.   |
|---------------------------------------------------------------|-----------------------------------------------------------------|

### *exception* an.bench.run.BenchError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The bench could not produce a row it would be honest to file.

### an.bench.run.GOLDEN_METRIC_KEY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'min_ssim_win8_vs_golden'*

Family B’s two rows, one in each block. Named here because the golden result
fills both from one comparison, and a reader has to be able to see that the
boolean and the number are the same evidence read two ways.

### an.bench.run.JUST_BLESSED_DETAIL *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'this run WROTE these goldens, so comparing against them is a tautology: the identity holds by construction and no code could have failed it. Run \`an bench\` again, without --bless, for a comparison that can fail.'*

Said when a run blessed the goldens it would otherwise have compared against.

### an.bench.run.MIN_PINNED_FRAMES_FOR_PAIRWISE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 2*

A pairwise minimum needs two frames; every fixture pins at least two, so on
a real capture this row is always measured (the render-side panel may not
be null — `tests/test_bench_capture.py`).

### an.bench.run.STAGE_METRIC_KEY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'stage_min_plane_ratio_gap'*

The pan measurement (an#111). Metric and tripwire come from ONE measurement,
as the golden pair does: the boolean and the number must be the same evidence
read two ways, or a reader has to reconcile them.

### an.bench.run.STAGE_MIN_RATIO_GAP *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.1875*

The floor the tripwire fires below, set at HALF the first bless’s measured
minimum — the `expression_min_pairwise_changed_px` precedent followed
literally.

Measured on `stage_pan` at the first bless: **0.375**. Ratios are taken
against the largest mover, so depths 0.25 / 1.0 / 2.0 report as
0.125 / 0.5 / 1.0 and the closest pair is far/mid. (An earlier draft of this
comment said 0.75, which is the same measurement taken against the
`depth == 1` plane — a reference the pixel half cannot see, which is why
`_reference` is now always the largest mover.)

### an.bench.run.conversion_distance(frames_dir, lossless_mp4, , height, width, frames)

How far this build’s PNG->YUV conversion sits from the encoder’s own.

Recorded, never gated. It was a gate — a hard equality — and it failed on
the Linux runner while passing here, which is precisely the shape of a
machine-dependent fact masquerading as a universal one. Now the metrics no
longer depend on it, and the number is kept because it is the thing that
will explain a future cross-build surprise.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.run.format_panel(ledger)

A human-readable digest of a row — the thing `an bench` prints.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.bench.run.lossless_reference(frames_dir, fps, out, , delivered)

Encode the frames losslessly and return the decode — the encoder’s input.

Returned as a path rather than an array so the caller controls its
lifetime; every encode-side reference comes from here.

`delivered` is the delivered mp4 this leg is the reference FOR, and
passing it is what makes the leg’s pixel format match — \*\*probed off the
file rather than re-derived\*\* (an#72). Re-deriving is not enough: the
delivered encode resolves its format from `RenderContext.pix_fmt` *or*
from `render.DEFAULT_PIX_FMT`, so a leg that consults the global tracks
the bench’s lever and silently misses `an render --pix-fmt yuv444p`,
which sets the context instead. Only the file knows which seam won.

`delivered` is keyword-only and has **no default**, deliberately — the
same argument `imageio._reshape` makes for its own `frames`: a default here
would let a caller opt out of the match by omission, and the failure is
invisible (a 4:2:0 reference silently measuring a 4:4:4 delivery). Passing
`None` explicitly means “no delivered file to match” and falls back to the
module default; that is right for a caller building a reference for its own
sake and wrong for the bench, which always has the file in hand.

### an.bench.run.naming_git_state(git, , blessed, root)

Which git state NAMES the row: the tree the run **left**, not the one it read.

[`run_bench()`](_autosummary/an.bench.run.html.md#an.bench.run.run_bench) reads `git_state` once, *before* the corpus loop,
because that is the tree the pixels came from. A `--bless` run then WRITES
into that same tree inside the loop — the golden PNGs, and a bless record
whose `blessed_at` moves on every run — so by the time the row is named
the two are no longer one fact.

Named under the pre-bless state, a bless on a clean tree lands as
`<date>-<sha>.json`: a filename claiming a commit whose tree that very run
then modified, which is exactly what the `-dirty` suffix exists to prevent
([`an.bench.paths.ledger_path()`](_autosummary/an.bench.paths.html.md#an.bench.paths.ledger_path)). So the row records both — `git` is
what rendered, `git_after_bless` is what the run left behind — and the
*filename* follows the second. A non-bless run re-reads nothing.

Deliberately a plain function of `blessed` rather than an inline
conditional: it is the whole of the fix, and a guard for it must not have to
render the corpus to reach it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.run.pinned_frames_min_pairwise_changed_px(capture, times)

`expression_min_pairwise_changed_px` for one scene: decode the pinned
frames from today’s render and take the minimum over every pair of the
count of pixels that differ (an#98). On a two-frame scene that is the
pair’s own change; `unavailable`, never zero, only when a scene pins a
single frame, which the fixture rule forbids.

* **Return type:**
  [`Value`](_autosummary/an.bench.ledger.html.md#an.bench.ledger.Value)

### an.bench.run.run_bench(, scenes=None, out=None, keep_render=None, write=True, bless='', golden_root=None, lossless_scratch_root=None, root=None)

Render the corpus, compute the panel, and (by default) write the row.

`bless` is the **reason** a re-bless is being made, and passing it is what
turns the run into a bless. One argument rather than a `--bless` flag plus
a `--reason` string, so “blessed with no recorded reason” — the failure
this rule exists to prevent — is not expressible.

`lossless_scratch_root` is forwarded to `_lossless_scratch_dir()` for
every scene (an#143) — a caller may point it at a shared parent to prove
that two concurrent lossless-leg encodes still get distinct scratch
directories under it; production code has no reason to pass it.

`golden_root` redirects where goldens are read and written, and it exists
because without it a test of the bless path has no choice but to overwrite
the committed corpus. That is not hypothetical: the first version of an#38’s
bless test did exactly that, replacing a real bless record’s reason with the
test’s own.

`root` is the source checkout whose corpus `scenes` names (default: `an`’s
own); a genre package runs its own corpus with its own checkout (`cutan.bench`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.bench.run.shot_policy_provenance(shots)

The per-shot COMPILE POLICIES a scene rendered under, keyed by shot id.

Additive scene-provenance facts read off each staged `scene_json`’s
`meta`; `shots` is any iterable of objects with `shot_id` and
`scene_json` (a [`ShotCapture`](_autosummary/an.bench.capture.html.md#an.bench.capture.ShotCapture), or a stand-in).

- `blink_phases` (an#88): the per-entity blink phase each shot was
  compiled with. A renamed corpus character re-phases every blink and
  moves every pixel metric; this makes that a visible diff in the row
  instead of an unexplained shift.
- `step_hz` (an#89): the stepped-timing policy each shot’s tweens were
  compiled under; `None` = smooth (the meta key is absent then, by the
  serializer). Stepping moves `scene_contract_sha256` by construction —
  the resampled keyframes ARE the contract — and this says the movement
  was a timing policy, not a mystery.

```pycon
>>> from types import SimpleNamespace as NS
>>> shot_policy_provenance([NS(shot_id="s1", scene_json={"meta": {"step_hz": 12.0, "blink_phases": {"a": 0.5}}}),
...                         NS(shot_id="s2", scene_json={"meta": {}})])
{'blink_phases': {'s1': {'a': 0.5}, 's2': {}}, 'step_hz': {'s1': 12.0, 's2': None}, 'viseme_keyframes_per_second': {'s1': None, 's2': None}}
```

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.bench.run.viseme_keyframes_per_second(scene_json)

Viseme keyframes per second of dialogue in a compiled shot, or `None`
when nothing speaks (an#97).

Counted over the `__viseme__` clips: keyframes minus one per channel
(the trailing rest is an invariant, not a decision — today’s emission has
one channel per clip), over the clips’ summed durations, which are the
frame-ceiled windows rather than the lines’ exact lengths (a small
downward bias, the same for every row). Keyframes, not distinct shapes — a repeated code (the carried
rest before the terminal one) counts. The co-articulation passes bring
this below the RAW provider track’s rate; against the old drop-not-hold
condenser it can rise, because that loop was cheaper only by dropping
shapes. Recorded as provenance rather than a panel metric because no lever
in the registry moves it.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> viseme_keyframes_per_second({"animations": {}})
>>> viseme_keyframes_per_second({"animations": {"__viseme__s_0_m": {"duration": 2.0,
...     "channels": [{"keyframes": [{"time": 0}, {"time": 0.5}, {"time": 1.0}, {"time": 2.0}]}]}}})
1.5
```


# _autosummary/an.bench.stage.html.md

# an.bench.stage

Measuring a pan: does the stage move as planes, or as one rigid image?

**The trap the epic’s own sentence sets.** “The planes moved at different
rates” is satisfied by a scene with *no parallax whatsoever*, because a
centre-anchored zoom already produces unequal per-plane displacements. The
honest null hypothesis is **“the camera moved the whole stage as one rigid
image”**, and a zoom has to be excluded rather than assumed away.

**The quantity.** Probe each plane at scene-space `x = 0` — the canvas
centre column. There the zoom term vanishes *exactly*:

> Δ_i = s₁ · p_i · D₁          ratio_ij = Δ_i / Δ_j = p_i / p_j   (any zoom)

Under a rigid pan every ratio is exactly 1, so the measurement can tell a
parallaxing stage from a zooming one. Two additions the epic does not require:

- assert `Δy ≈ 0`, so a pan is distinguishable from a zoom — but only for a
  ZOOM-FREE pan: §4 endorses zoom composing through the pivot, and a pan+zoom
  shot measures a non-zero Δy by design;
- assert the ORDERING `p_far < p_mid < p_near`, because wrong-order parallax
  is a real bug that a bare inequality passes. It is a \*\*bonus, not a second
  gate\*\*: the zoom false positive satisfies the ordering too. Only the x = 0
  probe excludes a zoom.

Two measurements, deliberately different instruments:

**(a) JSON** — free, on every PR, from the compiled document through
[`an.stage.timeline.screen_position()`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.screen_position). Composed screen space,
not local channel values: a rigid pan on `root` leaves every plane’s local
`Δx` at zero.

**(b) Pixels** — on a labelled PR, per-plane centroids over exact-colour
masks. The `x = 0` cancellation does **not** reach (b): a centroid sits at
the plane’s own offset, not at `x = 0`, so the fixture holds zoom constant.
And each mask’s pixel COUNT is asserted unchanged between frames, because a
plane panning partly off-canvas biases its centroid — measured, that read a
`depth = 2` plane as 1.975.

### Functions

| [`measure_pan_json`](_autosummary/an.bench.stage.html.md#an.bench.stage.measure_pan_json)(scene, plane_paths, times, \*)   | Measurement (a): composed screen displacement, probed at `x = 0`.    |
|----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`plane_centroids`](_autosummary/an.bench.stage.html.md#an.bench.stage.plane_centroids)(frame, colours)                   | `{plane: (cx, cy, pixel count)}` for exact-colour masks.             |
| [`measure_pan_pixels`](_autosummary/an.bench.stage.html.md#an.bench.stage.measure_pan_pixels)(frames, colours, \*[, depths]) | Measurement (b): per-plane centroid displacement between two frames. |
| [`min_ratio_gap`](_autosummary/an.bench.stage.html.md#an.bench.stage.min_ratio_gap)(ratios)                             | The smallest gap between any two planes' ratios.                     |

### Classes

| [`PlaneTrack`](_autosummary/an.bench.stage.html.md#an.bench.stage.PlaneTrack)(name, dx, dy[, depth])   | One plane's displacement between the two probed times.   |
|--------------------------------------------------------------------------------------|----------------------------------------------------------|
| [`PanMeasurement`](_autosummary/an.bench.stage.html.md#an.bench.stage.PanMeasurement)(tracks, reference)   | What a pan did, plane by plane.                          |

### Exceptions

| [`RotatingCamera`](_autosummary/an.bench.stage.html.md#an.bench.stage.RotatingCamera)   | The camera rolls, so a per-axis ratio is not a depth ratio.   |
|-------------------------------------------------------------------|---------------------------------------------------------------|

### *class* an.bench.stage.PanMeasurement(tracks, reference)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a pan did, plane by plane.

#### *property* is_rigid *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when every plane moved by the same amount — the null
hypothesis, and what a stage with no parallax looks like.

Computed from the DISPLACEMENTS, not from the ratios, because a stage
that did not move at all has no ratios (the reference is zero, so every
ratio is NaN) and is nonetheless as rigid as a stage can be. Reading it
off the ratios made a pure zoom — which cancels to zero displacement at
the probe column, exactly as intended — report as parallaxing.

```pycon
>>> PanMeasurement((PlaneTrack("a", 10.0, 0.0), PlaneTrack("b", 10.0, 0.0)), "b").is_rigid
True
>>> PanMeasurement((PlaneTrack("a", 0.0, 0.0), PlaneTrack("b", 0.0, 0.0)), "b").is_rigid
True
>>> PanMeasurement((PlaneTrack("a", 5.0, 0.0), PlaneTrack("b", 10.0, 0.0)), "b").is_rigid
False
```

#### *property* ratios *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [float](https://docs.python.org/3/builtins/functions.html#float)]*

`{plane: Δ_i / Δ_ref}`. A rigid stage gives 1.0 for every plane.

```pycon
>>> m = PanMeasurement((PlaneTrack("a", 10.0, 0.0), PlaneTrack("b", 40.0, 0.0)), "b")
>>> m.ratios
{'a': 0.25, 'b': 1.0}
```

#### reference *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

always the largest mover, so
the JSON and pixel halves agree and the number does not depend on what
a descriptor declares (`_reference()`).

* **Type:**
  The plane every ratio is taken against

### *class* an.bench.stage.PlaneTrack(name, dx, dy, depth=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One plane’s displacement between the two probed times.

#### depth *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The declared parallax factor, when the caller knows it.

### *exception* an.bench.stage.RotatingCamera

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The camera rolls, so a per-axis ratio is not a depth ratio.

Under a rotation the composed x-displacement is `−fx·A + fy·B` with
plane-independent `A`/`B`, so the axes mix and `Δ_i/Δ_j` stops being
`f_i/f_j`. Measured with `rotation = 0.6` and per-axis factors: one plane
reported a NEGATIVE ratio and the resulting gap was *larger* than the
honest one, so the tripwire passed on a number that meant nothing
(an#111 review, M2).

Refused rather than reported. The measurement’s whole job is to exclude a
camera move that mimics depth; silently reporting one is the failure it
exists to prevent, pointed the other way.

### an.bench.stage.measure_pan_json(scene, plane_paths, times, , depths=None)

Measurement (a): composed screen displacement, probed at `x = 0`.

`plane_paths` are full node paths (`"depths/far"`); the returned
tracks are keyed by the LAST segment, which is the plane’s own name.

* **Return type:**
  [`PanMeasurement`](_autosummary/an.bench.stage.html.md#an.bench.stage.PanMeasurement)

### an.bench.stage.measure_pan_pixels(frames, colours, , depths=None)

Measurement (b): per-plane centroid displacement between two frames.

* **Return type:**
  [`PanMeasurement`](_autosummary/an.bench.stage.html.md#an.bench.stage.PanMeasurement)

### an.bench.stage.min_ratio_gap(ratios)

The smallest gap between any two planes’ ratios.

This is the number the ledger row reports and the tripwire guards. Zero
means two planes moved together, which on a stage that declares distinct
depths means the parallax flattened.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> min_ratio_gap({"far": 0.25, "mid": 1.0, "near": 2.0})
0.75
>>> min_ratio_gap({"far": 1.0, "mid": 1.0})
0.0
```

### an.bench.stage.plane_centroids(frame, colours)

`{plane: (cx, cy, pixel count)}` for exact-colour masks.

`an/bench/masks.py` is deliberately not reused: it has no colour
selection. The primitive is `metrics.pack_rgb` plus equality — one integer
per colour, compared exactly, so a plane’s mask cannot pick up an
anti-aliased edge pixel from its neighbour.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`int`](https://docs.python.org/3/builtins/functions.html#int)]]


# _autosummary/an.build.cli.html.md

# an.build.cli

`an cache …` — the shot cache from the shell (an#274).

Wired into the top-level dispatcher as the `cache` namespace
(`an.tools._dispatch_namespaces`), programmatically, per pillar 8: plain
functions taking strings and booleans and returning the text to print; the
business logic is [`an.build.gc`](_autosummary/an.build.gc.html.md#module-an.build.gc).

Subcommands: `info` (size, entries, how much the current scene reaches) and
`gc` (delete what nothing reaches; `--dry-run` first).

### Functions

| [`gc`](_autosummary/an.build.cli.html.md#an.build.cli.gc)(project_dir[, dry_run, max_size, ...])   | Delete the shot-cache entries nothing reaches: not the current scene (under the default render settings or any a recorded render used), and not the latest render of each output on each machine.   |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`info`](_autosummary/an.build.cli.html.md#an.build.cli.info)(project_dir[, no_reachability])        | Show the project's shot cache: its size, its entries by kind, and how much of it the current scene still reaches.                                                                                   |

### an.build.cli.gc(project_dir, dry_run=False, max_size='', max_age='', force=False)

Delete the shot-cache entries nothing reaches: not the current scene (under the default render settings or any a recorded render used), and not the latest render of each output on each machine.

Never deletes a reachable entry, nor anything written since a render of this project still in progress began. A render running meanwhile can at worst re-render a shot, never use a wrong one.

project_dir: path to an an project
dry_run: report what would be deleted, delete nothing
max_size: keep the most recently written unreachable entries that fit in a cache of this size (e.g. 2G, 500MB) — the shot cache and the Manim stores together; reachable entries are never removed, so the cache can stay above it
max_age: keep only the unreachable entries written within this age (e.g. 7d, 36h); a recorded render older than it stops naming its entries, but the current scene under its settings is still kept (and when the current scene cannot be keyed under its settings, a line’s audio not cached under them, its entries are kept whatever its age)
force: collect a cache no render of this project has recorded what it used in (one written before `an cache gc` existed): keep only what the current scene reaches (a setting whose audio is no longer cached is not kept; an#311)

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.build.cli.info(project_dir, no_reachability=False)

Show the project’s shot cache: its size, its entries by kind, and how much of it the current scene still reaches.

project_dir: path to an an project
no_reachability: skip computing what the current scene reaches (which compiles every shot and probes this machine’s browser, like a render’s first second)

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.build.derived.html.md

# an.build.derived

A renderer’s derived stores, as `an cache gc` collects them (an#299).

An opaque renderer keeps intermediates of its own beside the shot cache — Manim
its raw pictures, their measurement records and contact sheets — content-keyed,
so every edit adds entries and nothing removes them. A renderer declares them
here, beside its shot keyer ([`an.build.keys.register_shot_keyer()`](_autosummary/an.build.keys.html.md#an.build.keys.register_shot_keyer)), with
[`register_derived_stores()`](_autosummary/an.build.derived.html.md#an.build.derived.register_derived_stores); [`an.build.gc`](_autosummary/an.build.gc.html.md#module-an.build.gc) collects every registered
store under the shot cache’s guarantees.

A declaration ([`DerivedStores`](_autosummary/an.build.derived.html.md#an.build.derived.DerivedStores)) names:

- `record_store` — the store whose entries NAME others (Manim’s
  `measurements`: a record names its picture and its contact sheet), deleted
  first, so a record never names something already gone;
- `named_stores` — the stores records name (`pictures`,
  `contact_sheets`);
- `entries(renderer, shot, ctx)` — the record-store keys a render of `shot`
  under `ctx` reads, COMPUTED (never rendered);
- `provenance(render_provenance)` — the entries a cached shot’s provenance
  names, by store;
- `names(record_bytes)` — what one record names, by store; it raises
  [`UnreadableRecordError`](_autosummary/an.build.derived.html.md#an.build.derived.UnreadableRecordError) for bytes it cannot read (a record half
  > written by a concurrent render), and the collector then deletes nothing a
  > record could name.

A store belongs to one renderer: a second claim is refused, so one renderer’s
`names` never reads another’s records.

```pycon
>>> spec = DerivedStores(
...     record_store="records", named_stores=("blobs",),
...     entries=lambda renderer, shot, ctx: set(),
...     provenance=lambda provenance: {},
...     names=lambda data: {"blobs": {data.decode()}},
... )
>>> sorted(spec.stores)
['blobs', 'records']
>>> sorted(spec.closure({"records": {"r": b"b1"}}, {"records": {"r"}})["blobs"])
['b1']
```

### Functions

| `derived_stores_for`(renderer_name)                                                               |                                                             |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| [`register_derived_stores`](_autosummary/an.build.derived.html.md#an.build.derived.register_derived_stores)(renderer_name, spec, \*) | Declare `renderer_name`'s derived stores for `an cache gc`. |
| [`registered_derived_stores`](_autosummary/an.build.derived.html.md#an.build.derived.registered_derived_stores)()                      | Every declaration, by renderer name.                        |

### Classes

| [`DerivedStores`](_autosummary/an.build.derived.html.md#an.build.derived.DerivedStores)(record_store, named_stores, ...)   | One renderer's derived stores (see the module docstring).   |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------|

### Exceptions

| [`DerivedStoresRegistrationError`](_autosummary/an.build.derived.html.md#an.build.derived.DerivedStoresRegistrationError)   | A derived-store declaration that cannot be collected safely.   |
|-----------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`UnreadableRecordError`](_autosummary/an.build.derived.html.md#an.build.derived.UnreadableRecordError)            | A record's bytes cannot be read (half written, damaged).       |

### *class* an.build.derived.DerivedStores(record_store, named_stores, entries, provenance, names)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One renderer’s derived stores (see the module docstring).

#### closure(mall, entries)

`entries` plus what each of its records names. A record that is
absent names nothing; one that cannot be read raises
[`UnreadableRecordError`](_autosummary/an.build.derived.html.md#an.build.derived.UnreadableRecordError).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

#### *property* stores *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Every store, the record store first (the deletion order).

### *exception* an.build.derived.DerivedStoresRegistrationError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A derived-store declaration that cannot be collected safely.

### *exception* an.build.derived.UnreadableRecordError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A record’s bytes cannot be read (half written, damaged).

### an.build.derived.register_derived_stores(renderer_name, spec, , replace=False)

Declare `renderer_name`’s derived stores for `an cache gc`.

Refused: a store another renderer already claims, and a second
registration for the name unless `replace` (a module reloaded passes
the same stores again, which is allowed).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.build.derived.registered_derived_stores()

Every declaration, by renderer name. Loads the renderer registry first
(a backend behind the import firewall declares when it is imported), its
load warnings about unrelated backends silenced: they are the render’s
to give.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`DerivedStores`](_autosummary/an.build.derived.html.md#an.build.derived.DerivedStores)]


# _autosummary/an.build.gc.html.md

# an.build.gc

Garbage collection of the shot cache: `an cache gc` and `an cache info` (an#274).

ADR 0004 decision 6: invalidation is by digest, never by deletion, so every
edit leaves the old entries behind — an end user’s `artifacts/shot_cache`
reached 4.9 GB after nine renders of a 16 s film. Collecting them is a
separate, explicit command, and this module is it.

**What is kept (reachable).** An entry is reachable when either

- a render of the project’s CURRENT scene would read it — under any knob set
  a recorded render ever used, or under a plain `an render`’s — computed
  by the render loop’s own setup and the engine’s own key code, with the
  dialogue stamped from the audio stores as the render stamps it
  ([`an.render.cache_entries()`](_autosummary/an.render.html.md#an.render.cache_entries)); or
- the latest render of an output, under one knob set, on one machine, used it
  — its *root* ([`an.build.ShotCache.record_root()`](_autosummary/an.build.html.md#an.build.ShotCache.record_root)). This keeps what a
  render on ANOTHER machine of a synced project used, which this machine
  cannot recompute (its environment digest differs).

A cache no render of the project has recorded a root in (one written before
an#274) is refused unless `force`: the current scene’s keys alone are then
the only evidence, and a render’s knobs or providers that differ from the
defaults would leave its entries looking unreachable.

**A knob set under which the current scene has a line with no audio** (an#306)
— a line edited since the last render under it, or the plain render’s
hypothetical knobs for a project only ever spoken by ElevenLabs — has no keys
to compute until that line is synthesised, so nothing in the cache can be its
entry for that shot. It is skipped rather than refused, and what each root
recorded under it is kept whatever `--max-age` says, so its unchanged shots
stay. Only when the current scene can be keyed under NO knob set (every one
lacks some line’s audio: the scene has lines nothing has synthesised) is the
collection refused — render first. Each knob set is replayed with its OWN
providers (a root’s recorded `tts`), never with a command line’s defaults.

Everything else is unreachable: the entries of shots as they were before an
edit, parts cut for an old neighbour, whole-frame entries (`<key>.frames`)
that no render reads since an#260, and unreadable records.

**What is never deleted.** A reachable entry, whatever the caps say (a cap
trims unreachable history only; `--max-age` lets an old root stop naming its
entries, never stops its knob set being recomputed). A root. An entry
written after the collection began, or after the start of any render of the
project still in progress (`.an/render_work/runs/<run>/.live`), minus
[`CLOCK_SLACK_S`](_autosummary/an.build.gc.html.md#an.build.gc.CLOCK_SLACK_S): a render’s entries are protected from the moment its
run starts until its root records them. A blob still named by a kept record.

**What a concurrent render can see**, at worst: an entry it looked up being
collected between reading its record and its blob — a miss, so that shot
renders again. Never a wrong picture: every blob is checked against its id
(its sha256) when read, and a reused shot’s bytes are copied into the render’s
own run directory at lookup, before anything can remove them.

**Deletion is permanent.** `dol.Files` would move each file to the OS trash,
which frees nothing (and on macOS asks Finder once per file), so on a
filesystem store the catalog record and the blob are unlinked directly, in the
layout `lacing.ArtifactStore.from_directory` documents.

```pycon
>>> parse_size("2G"), parse_size("500MB"), parse_age("36h"), parse_age("7d")
(2147483648, 524288000, 129600.0, 604800.0)
```

### Module Attributes

| [`CLOCK_SLACK_S`](_autosummary/an.build.gc.html.md#an.build.gc.CLOCK_SLACK_S)         | a file system's timestamp granularity, and the gap between a record's provenance time and the moment it lands.                                                                                      |
|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DEFAULT_PROFILE`](_autosummary/an.build.gc.html.md#an.build.gc.DEFAULT_PROFILE)       | The render knobs of a plain `an render` (and of `render_project`'s defaults), as `ShotCache.record_root()` records them: `tts` is each voice's own provider (an#305).                               |
| [`HYPOTHETICAL_PROFILES`](_autosummary/an.build.gc.html.md#an.build.gc.HYPOTHETICAL_PROFILES) | today's ([`DEFAULT_PROFILE`](_autosummary/an.build.gc.html.md#an.build.gc.DEFAULT_PROFILE)) and the one before an#305, which spoke every line offline — what a cache written before roots existed was rendered with. |

### Functions

| [`cache_info`](_autosummary/an.build.gc.html.md#an.build.gc.cache_info)(project_dir, \*[, reachability, ...])   | What `project_dir`'s shot cache holds; never fails on reachability (it says why it is unknown instead).   |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| [`collect_garbage`](_autosummary/an.build.gc.html.md#an.build.gc.collect_garbage)(project_dir, \*[, dry_run, ...])   | Delete the shot-cache entries of `project_dir` that nothing reaches.                                      |
| [`inventory`](_autosummary/an.build.gc.html.md#an.build.gc.inventory)(store)                                   | Every catalog record of `store`, read once.                                                               |
| [`parse_age`](_autosummary/an.build.gc.html.md#an.build.gc.parse_age)(text)                                    | `"7d"`, `"36h"`, `"90m"`, `"2w"`, `"30s"` → seconds.                                                      |
| [`parse_size`](_autosummary/an.build.gc.html.md#an.build.gc.parse_size)(text)                                   | `"2G"`, `"500MB"`, `"1.5GB"`, `"1048576"` → bytes (1024-based).                                           |
| [`reachable_entries`](_autosummary/an.build.gc.html.md#an.build.gc.reachable_entries)(project, store, \*[, ...])       | What the project's current scene and its recorded roots reach.                                            |

### Classes

| [`CacheEntry`](_autosummary/an.build.gc.html.md#an.build.gc.CacheEntry)(id, role[, asset_id, bytes_size, ...])   | One catalog record: its id, what it holds, its blob, and when it was written.            |
|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [`CacheInfo`](_autosummary/an.build.gc.html.md#an.build.gc.CacheInfo)(path[, total_bytes, by_role, ...])        | The shot cache's size, what it holds, and how much of it is reachable.                   |
| [`GcReport`](_autosummary/an.build.gc.html.md#an.build.gc.GcReport)(dry_run[, deleted, deleted_blobs, ...])    | What a collection deleted (or, with `dry_run`, would delete), and why the rest was kept. |
| [`Reachability`](_autosummary/an.build.gc.html.md#an.build.gc.Reachability)([from_scene, from_roots, ...])         | What the current scene and the recorded roots keep, and why.                             |

### Exceptions

| [`CacheGcError`](_autosummary/an.build.gc.html.md#an.build.gc.CacheGcError)   | The collection cannot be done safely; nothing was deleted.   |
|-----------------------------------------------------------------|--------------------------------------------------------------|

### an.build.gc.CLOCK_SLACK_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 2.0*

a file
system’s timestamp granularity, and the gap between a record’s provenance
time and the moment it lands.

* **Type:**
  Seconds before a protection horizon still treated as “after” it

### *class* an.build.gc.CacheEntry(id, role, asset_id=None, bytes_size=0, written_at=None, record=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One catalog record: its id, what it holds, its blob, and when it was written.

### *exception* an.build.gc.CacheGcError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The collection cannot be done safely; nothing was deleted.

### *class* an.build.gc.CacheInfo(path, total_bytes=0, by_role=<factory>, reachable=None, unreachable=None, orphan_blobs=(0, 0), roots=<factory>, reachability_error='', skipped=<factory>, derived=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The shot cache’s size, what it holds, and how much of it is reachable.

#### derived *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), DerivedUsage]*

The renderers’ derived stores (an#299), by store name.

### an.build.gc.DEFAULT_PROFILE *: [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]* *= {'capture': None, 'fps': None, 'language': 'en', 'lipsync': 'offline', 'pix_fmt': None, 'resolution': None, 'step_hz': None, 'strict_assets': False, 'supersample': 1, 'tts': 'voice'}*

The render knobs of a plain `an render` (and of `render_project`’s
defaults), as `ShotCache.record_root()` records them: `tts` is each
voice’s own provider (an#305). Always among the profiles the current scene
is keyed under, so a cache written before roots existed keeps what a plain
render of the current scene reads.

### *class* an.build.gc.GcReport(dry_run, deleted=<factory>, deleted_blobs=<factory>, kept_reachable=0, kept_protected=<factory>, kept_retained=0, failed=<factory>, bytes_before=0, reach=None, deleted_derived=<factory>, derived_freed=0, kept_derived=0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a collection deleted (or, with `dry_run`, would delete), and why
the rest was kept.

#### deleted_derived *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]]*

what was (or would be) deleted,
by store, and the bytes that frees.

* **Type:**
  The renderers’ derived stores (an#299)

### an.build.gc.HYPOTHETICAL_PROFILES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)], ...]* *= ({'capture': None, 'fps': None, 'language': 'en', 'lipsync': 'offline', 'pix_fmt': None, 'resolution': None, 'step_hz': None, 'strict_assets': False, 'supersample': 1, 'tts': 'voice'}, {'capture': None, 'fps': None, 'language': 'en', 'lipsync': 'offline', 'pix_fmt': None, 'resolution': None, 'step_hz': None, 'strict_assets': False, 'supersample': 1, 'tts': 'offline'})*

today’s
([`DEFAULT_PROFILE`](_autosummary/an.build.gc.html.md#an.build.gc.DEFAULT_PROFILE)) and the one before an#305, which spoke every line
offline — what a cache written before roots existed was rendered with.

* **Type:**
  The knob sets a plain render used or uses, recorded by a root or not

### *class* an.build.gc.Reachability(from_scene=<factory>, from_roots=<factory>, profiles=<factory>, roots=<factory>, skipped=<factory>, derived=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What the current scene and the recorded roots keep, and why.

`profiles` are the knob sets the current scene was keyed under;
`skipped` the ones it could not be (`(profile, why)`: a line’s audio
is not cached under it — an#306), whose roots keep what they recorded.

#### derived *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [set](https://docs.python.org/3/builtins/stdtypes.html#set)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]]*

The renderers’ derived-store entries the current scene reads, by store
(a Manim picture, its measurement and contact sheet; an#299).

### an.build.gc.cache_info(project_dir, , reachability=True, engine=None, now=None)

What `project_dir`’s shot cache holds; never fails on reachability
(it says why it is unknown instead).

* **Return type:**
  [`CacheInfo`](_autosummary/an.build.gc.html.md#an.build.gc.CacheInfo)

### an.build.gc.collect_garbage(project_dir, , dry_run=False, max_size=None, max_age=None, engine=None, now=None, force=False)

Delete the shot-cache entries of `project_dir` that nothing reaches.

With no cap, every unreachable entry goes. With caps, unreachable history
is kept within them, newest first, and both bind: `max_age` (seconds)
keeps only unreachable entries written more recently than that (and lets a
recorded render older than that stop protecting its entries); `max_size`
(bytes) keeps only those that fit in a cache of that size. Neither ever
removes a reachable entry, so the cache can stay above `max_size`.
`dry_run` reports and deletes nothing. `force` collects a cache no
render of this project has recorded a root in (see [`reachable_entries()`](_autosummary/an.build.gc.html.md#an.build.gc.reachable_entries)).

See the module docstring for the reachability and concurrency argument.

* **Return type:**
  [`GcReport`](_autosummary/an.build.gc.html.md#an.build.gc.GcReport)

### an.build.gc.inventory(store)

Every catalog record of `store`, read once. An unreadable record is
listed with role `"unreadable"` and, on disk, its file’s mtime.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`CacheEntry`](_autosummary/an.build.gc.html.md#an.build.gc.CacheEntry)]

### an.build.gc.parse_age(text)

`"7d"`, `"36h"`, `"90m"`, `"2w"`, `"30s"` → seconds.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> parse_age("90m")
5400.0
```

### an.build.gc.parse_size(text)

`"2G"`, `"500MB"`, `"1.5GB"`, `"1048576"` → bytes (1024-based).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> parse_size("1.5k")
1536
```

### an.build.gc.reachable_entries(project, store, , entries=None, engine=None, root_max_age=None, now=None, force=False)

What the project’s current scene and its recorded roots reach.

The current scene is keyed under the knobs of EVERY recorded root (of any
project, any age: a knob set is a few values, and dropping one would orphan
that render’s entries of the unchanged scene), each with its own recorded
providers, and under [`HYPOTHETICAL_PROFILES`](_autosummary/an.build.gc.html.md#an.build.gc.HYPOTHETICAL_PROFILES) (a plain render’s). A
root younger than `root_max_age` seconds (all, when `None`) also keeps
the entries it names; roots themselves are always kept.

A knob set under which some line’s audio is not cached cannot be keyed
without a synthesis (an#306): it is skipped, listed in `skipped`, and
every root recorded under it keeps its entries whatever `root_max_age`.

`engine` computes the keys (its environment seam included); `None` is
a default [`ShotCache`](_autosummary/an.build.html.md#an.build.ShotCache) over `store`, which probes this
machine like a render does. Raises [`CacheGcError`](_autosummary/an.build.gc.html.md#an.build.gc.CacheGcError) when the current
scene’s keys cannot be computed for any other reason, or under no knob set
at all (guessing is never safe), and when no render of THIS project has
recorded a root yet — a cache written before roots existed (an#274) —
unless `force`.

* **Return type:**
  [`Reachability`](_autosummary/an.build.gc.html.md#an.build.gc.Reachability)


# _autosummary/an.build.html.md

# an.build

Incremental re-processing: content-addressed build stages (ADR 0004).

A draft must stay adjustable without re-processing everything: an edit
re-renders only what depends on it. The first slice is the **shot cache** —
`an render` skips any shot whose key (everything its render reads, digested;
never `shot.id`) already has an entry, and reuses that entry’s mp4.

- [`an.build.keys`](_autosummary/an.build.keys.html.md#module-an.build.keys) — canonical digests, the project-wide fallback
  dependency, and the registry through which a renderer says what its shot
  render reads ([`register_shot_keyer()`](_autosummary/an.build.html.md#an.build.register_shot_keyer)).
- [`an.build.reads`](_autosummary/an.build.reads.html.md#module-an.build.reads) — what a shot read: the recording view of the mall a
  keyer compiles against, and the digests of the entries it saw (an#316).
- [`an.build.shot_cache`](_autosummary/an.build.shot_cache.html.md#module-an.build.shot_cache) — the `incremental=` seam
  ([`IncrementalEngine`](_autosummary/an.build.html.md#an.build.IncrementalEngine)), its built-in engine [`ShotCache`](_autosummary/an.build.html.md#an.build.ShotCache), and
  the entries, shaped as `lacing` artifacts in a `lacing.ArtifactStore`.
- [`an.build.gc`](_autosummary/an.build.gc.html.md#module-an.build.gc) — garbage collection: what the current scene and the
  recorded renders reach, and deleting the rest (`an cache gc`,
  `an cache info`; [`an.build.cli`](_autosummary/an.build.cli.html.md#module-an.build.cli)).

The core names no renderer; the cut-out keyer lives with the cut-out backend
(`an.stage.cache_key`) and registers on its import.

```pycon
>>> from an.build import ShotCache, resolve_incremental
>>> isinstance(resolve_incremental(True), ShotCache)
True
```

### Functions

| [`cache_info`](_autosummary/an.build.html.md#an.build.cache_info)(project_dir, \*[, reachability, ...])   | What `project_dir`'s shot cache holds; never fails on reachability (it says why it is unknown instead).                                                          |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`canonical_digest`](_autosummary/an.build.html.md#an.build.canonical_digest)(obj)                              | The hex sha256 of `canonical_json()` of `obj`.                                                                                                                   |
| [`collect_garbage`](_autosummary/an.build.html.md#an.build.collect_garbage)(project_dir, \*[, dry_run, ...])   | Delete the shot-cache entries of `project_dir` that nothing reaches.                                                                                             |
| [`compose_shot_key`](_autosummary/an.build.html.md#an.build.compose_shot_key)(parts)                            | The shot key: one digest over the named parts and the key's own version.                                                                                         |
| [`every_asset_digest`](_autosummary/an.build.html.md#an.build.every_asset_digest)(mall, \*[, project_root])       | Every asset in the project (decision 3's fallback), without the root files — those are [`project_dependencies()`](_autosummary/an.build.html.md#an.build.project_dependencies)'. |
| [`default_environment_digest`](_autosummary/an.build.html.md#an.build.default_environment_digest)(renderer_name)          | The digest of `renderer_name`'s registered environment probe, once per process.                                                                                  |
| [`in_memory_shot_cache_store`](_autosummary/an.build.html.md#an.build.in_memory_shot_cache_store)()                       | A shot cache held in dicts — for tests, and for a mall with no disk.                                                                                             |
| [`project_assets_digest`](_autosummary/an.build.html.md#an.build.project_assets_digest)(mall, \*[, stores, ...])     | One digest over every asset store of the project (ADR 0004 decision 3).                                                                                          |
| [`project_dependencies`](_autosummary/an.build.html.md#an.build.project_dependencies)(mall, \*[, project_root])     | What every shot depends on project-wide: the project-root files (`PROJECT_ROOT_FILES`, the library lockfile), read by path.                                      |
| [`read_digests`](_autosummary/an.build.html.md#an.build.read_digests)(mall, reads)                          | `{"store/key": digest}` for every recorded read — the shot's dependency edges.                                                                                   |
| [`register_shot_keyer`](_autosummary/an.build.html.md#an.build.register_shot_keyer)(renderer_name, keyer, \*)      | Declare how shots of `renderer_name` are keyed, and how its machine is probed.                                                                                   |
| [`registered_shot_keyers`](_autosummary/an.build.html.md#an.build.registered_shot_keyers)()                           | The renderer names that have a keyer.                                                                                                                            |
| [`resolve_incremental`](_autosummary/an.build.html.md#an.build.resolve_incremental)(incremental)                   | `incremental=` → an engine, or `None` for "render every shot cold".                                                                                              |
| [`shot_artifact_type`](_autosummary/an.build.html.md#an.build.shot_artifact_type)()                               | The record type (`lacing.Artifact` subclass), built on first use.                                                                                                |
| [`shot_cache_store`](_autosummary/an.build.html.md#an.build.shot_cache_store)(root)                             | A filesystem shot cache under `root`: `catalog/` + `blobs/` (lacing's layout).                                                                                   |
| [`shot_keyer_for`](_autosummary/an.build.html.md#an.build.shot_keyer_for)(renderer)                           | The keyer that describes `renderer` (an instance, or a name), or `None`.                                                                                         |

### Classes

| [`RecordingMall`](_autosummary/an.build.html.md#an.build.RecordingMall)(mall, \*[, recorded])             | A view of `mall` that records which asset entries are read through it.        |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`BuildReport`](_autosummary/an.build.html.md#an.build.BuildReport)([outcomes, store_bytes, ...])       | Every shot's outcome, in timeline order.                                      |
| [`IncrementalEngine`](_autosummary/an.build.html.md#an.build.IncrementalEngine)(\*args, \*\*kwargs)           | The `incremental=` seam of `an.render.render` (ADR 0004 decision 5).          |
| [`ShotCache`](_autosummary/an.build.html.md#an.build.ShotCache)([store, environment, ...])            | The built-in engine: look a shot's key up in an ArtifactStore; record misses. |
| [`ShotKeyInputs`](_autosummary/an.build.html.md#an.build.ShotKeyInputs)(parts[, compile_s, details])      | What a renderer's keyer returns for one shot.                                 |
| [`ShotOutcome`](_autosummary/an.build.html.md#an.build.ShotOutcome)(shot_id, renderer, status[, ...])   | What happened to one shot in one render, with its wall times (seconds).       |
| [`ShotPlan`](_autosummary/an.build.html.md#an.build.ShotPlan)(shot_id, renderer, key[, inputs, ...]) | The engine's answer for one shot: its key, and what to reuse if anything.     |

### Exceptions

| [`CacheGcError`](_autosummary/an.build.html.md#an.build.CacheGcError)     | The collection cannot be done safely; nothing was deleted.                     |
|-------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`ShotCacheWarning`](_autosummary/an.build.html.md#an.build.ShotCacheWarning) | The shot cache could not do something it should have; the render still stands. |

### *class* an.build.BuildReport(outcomes=<factory>, store_bytes=None, store_entries=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Every shot’s outcome, in timeline order.

#### store_bytes *: [int](https://docs.python.org/3/builtins/functions.html#int) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

The shot cache’s size after the render, when the engine measured it.

#### store_line()

How big the shot cache is after the render, or `""` if unmeasured.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> BuildReport(store_bytes=3 * 1024**2, store_entries=4).store_line()
'3.0 MB in 4 entries'
```

#### summary()

One line: what was rendered, what reused, and WHY each rendered shot
was not reused — a cache that silently re-renders everything reads as
a broken cache (an#243 review, R2-1).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> BuildReport([ShotOutcome("a", "cutout", "uncached", reason=FRAMES_NOT_CACHED)]).summary()
'1 shot(s): 1 rendered (a), 0 reused (-); not reused: frames not cached: film has transitions/sound; pass --cache-frames (a)'
```

#### timing_table()

A Markdown table of the per-shot wall times.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### *exception* an.build.CacheGcError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The collection cannot be done safely; nothing was deleted.

### *class* an.build.IncrementalEngine(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

The `incremental=` seam of `an.render.render` (ADR 0004 decision 5).

`begin` once per render with the project mall (and its root, for the
root files every shot depends on); `plan` once per shot,
BEFORE any shot renders (in the calling thread); `record` once per shot
that was rendered (possibly from a worker thread); `finish` returns the
report.

Two hooks are OPTIONAL (the render loop calls them when an engine has
them): `record_parts(plan, parts)` once per rendered shot whose film
window is not whole (an#260), and `record_root(output_name, profile=...,
output=...)` once the film is delivered (what the garbage collector keeps).

### *class* an.build.RecordingMall(mall, , recorded=('characters', 'environments', 'props', 'styles', 'voices', 'sounds'))

Bases: [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)

A view of `mall` that records which asset entries are read through it.

Stores named in `recorded` come back wrapped in a `RecordingStore`
(one per store, so identity is stable across lookups); every other store
comes back as it is. [`reads`](_autosummary/an.build.reads.html.md#module-an.build.reads) is the set of `(store, key)` read so
far, `key` being `WHOLE_STORE` for a store that was listed.

### *class* an.build.ShotCache(store=None, \*, environment=<function default_environment_digest>, dependencies=<function project_dependencies>, fallback=<function every_asset_digest>, record_reads=True, cache_frames=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The built-in engine: look a shot’s key up in an ArtifactStore; record misses.

`store` is the injected `lacing.ArtifactStore`; `None` means the
mall’s `shot_cache` (resolved in `begin()`), and a mall without one
renders every shot. `environment(renderer_name) -> digest` is the
environment seam — injectable so a test (or a remote-render backend) can
state its machine rather than probe this one. Three dependency seams (see
`Dependencies`):

- `record_reads` (on by default) keys a shot whose keyer is registered
  with `records_reads=True` on the asset entries its compile read
  ([`an.build.reads`](_autosummary/an.build.reads.html.md#module-an.build.reads), an#316: the `assets` part), so an edit to an
  asset re-renders only the shots that read it;
- `fallback` is the `assets` part of every OTHER shot — a keyer that
  cannot vouch for its reads, or every shot under `record_reads=False`:
  by default every asset in the project (decision 3’s first slice);
- `dependencies` is what EVERY shot depends on (the `project` part): by
  default the library lockfile. `None` drops it, and `fallback=None`
  keys an unrecorded shot on its own parts alone — use either knowingly.

An engine built with other seams writes keys `an cache gc` (which
recomputes the current scene’s keys with the defaults) does not reach:
collect such a cache with the same engine.
`cache_frames` also stores each shot’s whole PNG sequence when a caller
plans with `needs_frames=True`. The render loop no longer does (an#260):
an assembled film takes a shot’s mp4 and, at a transition, its *parts*
(`window=`), which are cached by default at a few MB. Kept for callers of
the engine that need every frame; `an cache gc` collects old frames.

After a render, `report` holds what happened to each shot.

#### entry_ids(shot, renderer, ctx, , window=None)

The catalog ids a [`plan()`](_autosummary/an.build.html.md#an.build.ShotCache.plan) of this shot would read — computed by
the same code, looking nothing up and rendering nothing. What
`an.build.gc` keeps for the current scene. Call `begin()` first.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### plan(shot, renderer, ctx, , needs_frames=False, window=None, force=False)

`window` is what an assembled film needs from this shot
(`an.assemble.ShotWindow`; `None` for a plain concat): a shot whose
window is not whole is reused only when its parts entry is there too.

* **Return type:**
  [`ShotPlan`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotPlan)

#### record_parts(plan, parts)

Store a freshly rendered shot’s `an.assemble.ShotParts` under
`plan.parts_id`: its body mp4 and its window’s PNGs, one stored zip.
A failed write never fails the render (the next render renders it).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### record_root(output_name, , profile, output)

Record what this render used, as a ROOT for `an.build.gc`; return its id.

A root is keyed by the project (`project_id()`), the output name,
the render `profile` (the knobs as passed — `None` meaning “the
scene’s own”) and this machine (`machine_id()`), so the next render
of the same output with the same knobs on the same machine replaces it
— a browser upgrade included — while a render on another machine of a
synced project, or of another project sharing the store, keeps its own. Its record is a `lacing.Artifact` of the delivered film,
derived from the shot keys. Also measures the store, for the summary.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *exception* an.build.ShotCacheWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

The shot cache could not do something it should have; the render still stands.

### *class* an.build.ShotKeyInputs(parts, compile_s=None, details=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a renderer’s keyer returns for one shot.

`parts` are named sha256 hex digests (the key’s content half); the engine
adds `renderer`, `project` and `environment`. `compile_s` is the
wall time of the compile the keyer ran to get its digest, or `None` for a
renderer that has no compile stage.

#### details *: [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

Diagnostics a keyer wants on the record beside the digests — never part
of the key (the digests are).

### *class* an.build.ShotOutcome(shot_id, renderer, status, key=None, reason='', key_s=None, compile_s=None, render_s=None, cached_render_s=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What happened to one shot in one render, with its wall times (seconds).

`key_s` is the whole key computation, of which `compile_s` is the
compile; `render_s` is this render’s wall time (`None` when reused) and
`cached_render_s` the wall time of the render being reused.

### *class* an.build.ShotPlan(shot_id, renderer, key, inputs=<factory>, reads=<factory>, code=<factory>, cached=None, reason='', key_s=None, compile_s=None, cached_render_s=None, needs_frames=False, window=None, parts_id=None, parts=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The engine’s answer for one shot: its key, and what to reuse if anything.

#### code *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

The render path’s modules, digested (`{module: digest}`, an#395).

#### parts *: [Any](https://docs.python.org/3/library/typing.html#typing.Any)* *= None*

The reused parts (`an.assemble.ShotParts`), materialised for this plan.

#### parts_id *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

The catalog id of this shot’s parts entry, when its window is not whole.

#### reads *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

The asset entries the shot read, digested (`{"store/key": digest}`).

#### window *: [Any](https://docs.python.org/3/library/typing.html#typing.Any)* *= None*

What an assembled film needs from this shot (`an.assemble.ShotWindow`);
`None` for a film that is a plain concat of shot mp4s.

### an.build.cache_info(project_dir, , reachability=True, engine=None, now=None)

What `project_dir`’s shot cache holds; never fails on reachability
(it says why it is unknown instead).

* **Return type:**
  [`CacheInfo`](_autosummary/an.build.gc.html.md#an.build.gc.CacheInfo)

### an.build.canonical_digest(obj)

The hex sha256 of `canonical_json()` of `obj`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.build.collect_garbage(project_dir, , dry_run=False, max_size=None, max_age=None, engine=None, now=None, force=False)

Delete the shot-cache entries of `project_dir` that nothing reaches.

With no cap, every unreachable entry goes. With caps, unreachable history
is kept within them, newest first, and both bind: `max_age` (seconds)
keeps only unreachable entries written more recently than that (and lets a
recorded render older than that stop protecting its entries); `max_size`
(bytes) keeps only those that fit in a cache of that size. Neither ever
removes a reachable entry, so the cache can stay above `max_size`.
`dry_run` reports and deletes nothing. `force` collects a cache no
render of this project has recorded a root in (see `reachable_entries()`).

See the module docstring for the reachability and concurrency argument.

* **Return type:**
  [`GcReport`](_autosummary/an.build.gc.html.md#an.build.gc.GcReport)

### an.build.compose_shot_key(parts)

The shot key: one digest over the named parts and the key’s own version.

The parts are a MAPPING so the record can keep them by name, and a
re-render can be explained (”`textures` moved”) rather than merely
observed.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.build.default_environment_digest(renderer_name)

The digest of `renderer_name`’s registered environment probe, once per process.

Memoised per renderer NAME (each backend has its own machine: Chromium and
ffmpeg for cut-out, a Manim install for Manim), and once per process,
because the cut-out probe launches a browser and encodes a frame. So a
long-lived host does not see a `playwright install` or a `brew upgrade`
made after its first render: restart it, or pass `force_render`. A
renderer with no probe has the empty environment.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.build.every_asset_digest(mall, , project_root=None)

Every asset in the project (decision 3’s fallback), without the root
files — those are [`project_dependencies()`](_autosummary/an.build.html.md#an.build.project_dependencies)’. The `assets` part of a
shot whose keyer cannot vouch for its reads.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> every_asset_digest({"props": {"a": 1}}) != every_asset_digest({"props": {"a": 2}})
True
```

### an.build.in_memory_shot_cache_store()

A shot cache held in dicts — for tests, and for a mall with no disk.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.build.project_assets_digest(mall, , stores=('characters', 'environments', 'props', 'styles', 'voices', 'sounds'), project_root=None, root_files=('assets.lock.json',))

One digest over every asset store of the project (ADR 0004 decision 3).

The first slice’s dependency edge for every shot: safe — no asset can change
without every shot’s key moving — at the price of the per-character saving,
which read recording buys back. A store the mall does not have is recorded
as absent rather than skipped, so adding one later moves the digest.

With `project_root`, the files in `root_files` (the library lockfile)
are hashed by PATH as well — so a re-pin moves every key whether or not
the mall has a store for the file yet.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> a = project_assets_digest({"characters": {"c": {"v": 1}}}, stores=["characters"])
>>> b = project_assets_digest({"characters": {"c": {"v": 2}}}, stores=["characters"])
>>> a == b
False
```

### an.build.project_dependencies(mall, , project_root=None)

What every shot depends on project-wide: the project-root files
(`PROJECT_ROOT_FILES`, the library lockfile), read by path. A re-pin
changes what is checked out, so it moves every key, conservatively.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> project_dependencies({}) == project_dependencies({"props": {"a": 1}})
True
```

### an.build.read_digests(mall, reads)

`{"store/key": digest}` for every recorded read — the shot’s dependency edges.

A read of a store the mall does not have is recorded as `ABSENT`, so
the day the store appears the key moves.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> read_digests({}, [("props", "logo")])
{'props/logo': 'absent'}
```

### an.build.register_shot_keyer(renderer_name, keyer, , environment=None, renderer_type=None, records_reads=False, replace=False)

Declare how shots of `renderer_name` are keyed, and how its machine is probed.

The registration seam for every backend (cut-out here; Manim’s opaque
shots, keyed on source hash + Manim version + quality, are the next).
`renderer_type` binds the keyer to one renderer class: a renderer whose
type is not exactly it is never cached. `records_reads=True` claims
that every asset the shot depends on is read through `ctx.mall` (or is
already digested by one of the keyer’s parts): the engine then keys the
shot on the entries it read rather than on the whole project. A second registration for a name
is refused unless `replace=True` — a silent replacement would drop the
first keyer’s parts from every key without anyone saying so.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.build.registered_shot_keyers()

The renderer names that have a keyer.

A keyer registers beside its renderer, and a backend behind the import
firewall registers when the renderer registry first loads it (an#247), so
this loads the registry first.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> "cutout" in registered_shot_keyers()
True
```

### an.build.resolve_incremental(incremental)

`incremental=` → an engine, or `None` for “render every shot cold”.

`True` is a fresh [`ShotCache`](_autosummary/an.build.html.md#an.build.ShotCache) over the mall’s store; `False` or
`None` is off; anything else must be an [`IncrementalEngine`](_autosummary/an.build.html.md#an.build.IncrementalEngine).

* **Return type:**
  [`IncrementalEngine`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.IncrementalEngine) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> resolve_incremental(False) is None
True
>>> isinstance(resolve_incremental(True), ShotCache)
True
```

### an.build.shot_artifact_type()

The record type (`lacing.Artifact` subclass), built on first use.

### an.build.shot_cache_store(root)

A filesystem shot cache under `root`: `catalog/` + `blobs/` (lacing’s layout).

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.build.shot_keyer_for(renderer)

The keyer that describes `renderer` (an instance, or a name), or `None`.

`None` means the shot is never cached: no keyer for the name, or a
renderer whose class is not the one the keyer was registered for.

* **Return type:**
  `_KeyerEntry` | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### Modules

| [`cli`](_autosummary/an.build.cli.html.md#module-an.build.cli)               | `an cache …` — the shot cache from the shell (an#274).                                |
|----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| [`derived`](_autosummary/an.build.derived.html.md#module-an.build.derived)       | A renderer's derived stores, as `an cache gc` collects them (an#299).                 |
| [`gc`](_autosummary/an.build.gc.html.md#module-an.build.gc)                 | Garbage collection of the shot cache: `an cache gc` and `an cache info` (an#274).     |
| [`keys`](_autosummary/an.build.keys.html.md#module-an.build.keys)             | Cache keys for build stages: canonical digests, the project fallback, keyers.         |
| [`reads`](_autosummary/an.build.reads.html.md#module-an.build.reads)           | What a shot read: a read-recording view of the mall, and the digests of what it saw.  |
| [`shot_cache`](_autosummary/an.build.shot_cache.html.md#module-an.build.shot_cache) | The content-keyed shot cache: ADR 0004's first slice, behind the `incremental=` seam. |


# _autosummary/an.build.keys.html.md

# an.build.keys

Cache keys for build stages: canonical digests, the project fallback, keyers.

ADR 0004 decision 2: *every key covers everything that changes the output*, and
`shot.id` is never one of them (pillar 11). A shot key is composed from NAMED
parts, each a sha256 hex digest, so a re-render can say which input moved:

The core names no renderer: a backend joins by [`register_shot_keyer()`](_autosummary/an.build.keys.html.md#an.build.keys.register_shot_keyer). A
renderer with no keyer is never cached, which is the safe default — an opaque
shot is re-rendered, never reused on a guess.

```pycon
>>> canonical_digest({"b": 1, "a": [1, 2]}) == canonical_digest({"a": [1, 2], "b": 1})
True
>>> len(compose_shot_key({"renderer": canonical_digest("cutout")}))
64
```

### Module Attributes

| [`SHOT_KEY_IMPL_VERSION`](_autosummary/an.build.keys.html.md#an.build.keys.SHOT_KEY_IMPL_VERSION)       | The key's own version — the `impl_version` salt of `nw.Transform` and of `burns.RESOLVER_IMPL_VERSION` ("a lock, not a receipt").                                   |
|------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`PROJECT_ASSET_STORES`](_autosummary/an.build.keys.html.md#an.build.keys.PROJECT_ASSET_STORES)        | the art the compiler reads (characters, environments, props, styles) and the two the audio path reads (voices, sounds).                                             |
| [`PROJECT_ROOT_FILES`](_autosummary/an.build.keys.html.md#an.build.keys.PROJECT_ROOT_FILES)          | Files at the project ROOT that every shot depends on, whether or not a mall store exposes them.                                                                     |
| [`IGNORED_ASSET_NAME_PREFIXES`](_autosummary/an.build.keys.html.md#an.build.keys.IGNORED_ASSET_NAME_PREFIXES) | an OS's folder metadata must not re-render a film.                                                                                                                  |
| [`ABSENT`](_autosummary/an.build.keys.html.md#an.build.keys.ABSENT)                      | a missing texture, an audio ref the store does not hold.                                                                                                            |
| [`ShotKeyer`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyer)                   | `keyer(shot, ctx) -> ShotKeyInputs`.                                                                                                                                |
| [`EnvironmentProbe`](_autosummary/an.build.keys.html.md#an.build.keys.EnvironmentProbe)            | the renderer's environment record.                                                                                                                                  |
| [`ShotKeyPart`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyPart)                 | one more named digest for a renderer's key — the additive seam for an input read OUTSIDE the compiled document (a genre's side file, a vocabulary entry's version). |

### Functions

| [`bytes_digest`](_autosummary/an.build.keys.html.md#an.build.keys.bytes_digest)(data)                                 | The hex sha256 of `data`.                                                                                                                                                  |
|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`callable_identity`](_autosummary/an.build.keys.html.md#an.build.keys.callable_identity)(obj)                             | `module.qualname` of a function or class (of an instance: of its type).                                                                                                    |
| [`canonical_digest`](_autosummary/an.build.keys.html.md#an.build.keys.canonical_digest)(obj)                              | The hex sha256 of [`canonical_json()`](_autosummary/an.build.keys.html.md#an.build.keys.canonical_json) of `obj`.                                                                              |
| [`canonical_json`](_autosummary/an.build.keys.html.md#an.build.keys.canonical_json)(obj)                                | `obj` as sorted, whitespace-free JSON — the one spelling every digest hashes.                                                                                              |
| [`compose_shot_key`](_autosummary/an.build.keys.html.md#an.build.keys.compose_shot_key)(parts)                            | The shot key: one digest over the named parts and the key's own version.                                                                                                   |
| [`every_asset_digest`](_autosummary/an.build.keys.html.md#an.build.keys.every_asset_digest)(mall, \*[, project_root])       | Every asset in the project (decision 3's fallback), without the root files — those are [`project_dependencies()`](_autosummary/an.build.keys.html.md#an.build.keys.project_dependencies)'.           |
| [`file_digest`](_autosummary/an.build.keys.html.md#an.build.keys.file_digest)(path)                                  | The hex sha256 of a file's bytes, read NOW.                                                                                                                                |
| [`project_assets_digest`](_autosummary/an.build.keys.html.md#an.build.keys.project_assets_digest)(mall, \*[, stores, ...])     | One digest over every asset store of the project (ADR 0004 decision 3).                                                                                                    |
| [`project_dependencies`](_autosummary/an.build.keys.html.md#an.build.keys.project_dependencies)(mall, \*[, project_root])     | What every shot depends on project-wide: the project-root files ([`PROJECT_ROOT_FILES`](_autosummary/an.build.keys.html.md#an.build.keys.PROJECT_ROOT_FILES), the library lockfile), read by path. |
| [`project_root_files_digest`](_autosummary/an.build.keys.html.md#an.build.keys.project_root_files_digest)(project_root, \*[, ...]) | `{name: sha256 or ABSENT}` for the project-root files every shot depends on.                                                                                               |
| [`register_shot_key_part`](_autosummary/an.build.keys.html.md#an.build.keys.register_shot_key_part)(renderer_name, ...)         | Add one named input to every key of `renderer_name`'s shots — additively.                                                                                                  |
| [`register_shot_keyer`](_autosummary/an.build.keys.html.md#an.build.keys.register_shot_keyer)(renderer_name, keyer, \*)      | Declare how shots of `renderer_name` are keyed, and how its machine is probed.                                                                                             |
| [`registered_shot_keyers`](_autosummary/an.build.keys.html.md#an.build.keys.registered_shot_keyers)()                           | The renderer names that have a keyer.                                                                                                                                      |
| [`shot_keyer_for`](_autosummary/an.build.keys.html.md#an.build.keys.shot_keyer_for)(renderer)                           | The keyer that describes `renderer` (an instance, or a name), or `None`.                                                                                                   |
| [`store_digest`](_autosummary/an.build.keys.html.md#an.build.keys.store_digest)(store)                                | A digest of one store's whole content.                                                                                                                                     |

### Classes

| [`ShotKeyInputs`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyInputs)(parts[, compile_s, details])   | What a renderer's keyer returns for one shot.   |
|-----------------------------------------------------------------------------------------------|-------------------------------------------------|

### Exceptions

| [`ShotKeyerRegistrationError`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyerRegistrationError)   | A shot keyer or key part was registered twice, or collides with another.   |
|-------------------------------------------------------------------------------|----------------------------------------------------------------------------|

### an.build.keys.ABSENT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'absent'*

a missing texture, an
audio ref the store does not hold. Still deterministic, and different from
any present value, so a part that later appears re-renders.

* **Type:**
  A part’s value when the thing it digests is absent

### an.build.keys.EnvironmentProbe

the renderer’s environment record. Called once per
process per renderer (see `ShotCache`), because a browser probe costs a
launch.

* **Type:**
  `probe() -> Mapping`

alias of [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.build.keys.IGNORED_ASSET_NAME_PREFIXES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('.',)*

an OS’s folder
metadata must not re-render a film.

* **Type:**
  File names under an asset root that are never assets

### an.build.keys.PROJECT_ASSET_STORES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('characters', 'environments', 'props', 'styles', 'voices', 'sounds')*

the art the compiler reads (characters,
environments, props, styles) and the two the audio path reads (voices,
sounds). What a recording view records ([`an.build.reads`](_autosummary/an.build.reads.html.md#module-an.build.reads)), and what
“every asset in the project” means for decision 3’s fallback. The scene
document is deliberately NOT here — each shot’s own slice reaches its key
through its compiled document, which is what lets an edit to one shot
re-render only that shot.

* **Type:**
  The mall’s ASSET stores

### an.build.keys.PROJECT_ROOT_FILES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('assets.lock.json',)*

Files at the project ROOT that every shot depends on, whether or not a mall
store exposes them. `assets.lock.json` is the asset library’s lockfile of
pinned library versions (ADR 0005, P5): a re-pin changes what is checked out,
so it must move every key — and it must do so before (and independently of)
its registration in the mall (an#240). Read by path, through
[`project_root_files_digest()`](_autosummary/an.build.keys.html.md#an.build.keys.project_root_files_digest); an absent file is recorded as absent.

### an.build.keys.SHOT_KEY_IMPL_VERSION *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 2*

The key’s own version — the `impl_version` salt of `nw.Transform` and of
`burns.RESOLVER_IMPL_VERSION` (“a lock, not a receipt”). Bump it when the
COMPOSITION of a key changes (a part added, renamed or re-spelled). It is
NOT how a renderer’s code changes reach the key — a hand-bumped constant is
one someone forgets — that is each keyer’s `code` part, a digest of the
render path’s source. Bumping it orphans every entry; nothing is deleted
(decision 6).

### *class* an.build.keys.ShotKeyInputs(parts, compile_s=None, details=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a renderer’s keyer returns for one shot.

`parts` are named sha256 hex digests (the key’s content half); the engine
adds `renderer`, `project` and `environment`. `compile_s` is the
wall time of the compile the keyer ran to get its digest, or `None` for a
renderer that has no compile stage.

#### details *: [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

Diagnostics a keyer wants on the record beside the digests — never part
of the key (the digests are).

### an.build.keys.ShotKeyPart

one more named digest for a renderer’s key —
the additive seam for an input read OUTSIDE the compiled document (a genre’s
side file, a vocabulary entry’s version). See [`register_shot_key_part()`](_autosummary/an.build.keys.html.md#an.build.keys.register_shot_key_part).

* **Type:**
  `part(shot, ctx) -> str`

alias of [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.build.keys.ShotKeyer

`keyer(shot, ctx) -> ShotKeyInputs`. Raises what the render itself would
raise for the same inputs (a compile error, an invalid knob), so a bad shot
fails before any browser launches.

alias of [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`ShotKeyInputs`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyInputs)]

### *exception* an.build.keys.ShotKeyerRegistrationError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A shot keyer or key part was registered twice, or collides with another.

### an.build.keys.bytes_digest(data)

The hex sha256 of `data`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> bytes_digest(b"")[:8]
'e3b0c442'
```

### an.build.keys.callable_identity(obj)

`module.qualname` of a function or class (of an instance: of its type).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> callable_identity(callable_identity)
'an.build.keys.callable_identity'
```

### an.build.keys.canonical_digest(obj)

The hex sha256 of [`canonical_json()`](_autosummary/an.build.keys.html.md#an.build.keys.canonical_json) of `obj`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.build.keys.canonical_json(obj)

`obj` as sorted, whitespace-free JSON — the one spelling every digest hashes.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> canonical_json({"b": (1, 2), "a": None})
'{"a":null,"b":[1,2]}'
```

### an.build.keys.compose_shot_key(parts)

The shot key: one digest over the named parts and the key’s own version.

The parts are a MAPPING so the record can keep them by name, and a
re-render can be explained (”`textures` moved”) rather than merely
observed.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.build.keys.every_asset_digest(mall, , project_root=None)

Every asset in the project (decision 3’s fallback), without the root
files — those are [`project_dependencies()`](_autosummary/an.build.keys.html.md#an.build.keys.project_dependencies)’. The `assets` part of a
shot whose keyer cannot vouch for its reads.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> every_asset_digest({"props": {"a": 1}}) != every_asset_digest({"props": {"a": 2}})
True
```

### an.build.keys.file_digest(path)

The hex sha256 of a file’s bytes, read NOW.

Deliberately unmemoised. A (path, mtime, size) memo — `an.stage.raster`’s, which
is fine for a texture alias inside one compile — let a same-size edit whose
mtime was restored (`cp -p`, `rsync -t`, `tar x`, a sync client) be
served stale from cache in a long-running process (an#243 review, S2). A
cache KEY is only as good as its weakest input, so every byte is read on
every render; on the golden corpus the whole project digest is under 10 ms.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.build.keys.project_assets_digest(mall, , stores=('characters', 'environments', 'props', 'styles', 'voices', 'sounds'), project_root=None, root_files=('assets.lock.json',))

One digest over every asset store of the project (ADR 0004 decision 3).

The first slice’s dependency edge for every shot: safe — no asset can change
without every shot’s key moving — at the price of the per-character saving,
which read recording buys back. A store the mall does not have is recorded
as absent rather than skipped, so adding one later moves the digest.

With `project_root`, the files in `root_files` (the library lockfile)
are hashed by PATH as well — so a re-pin moves every key whether or not
the mall has a store for the file yet.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> a = project_assets_digest({"characters": {"c": {"v": 1}}}, stores=["characters"])
>>> b = project_assets_digest({"characters": {"c": {"v": 2}}}, stores=["characters"])
>>> a == b
False
```

### an.build.keys.project_dependencies(mall, , project_root=None)

What every shot depends on project-wide: the project-root files
([`PROJECT_ROOT_FILES`](_autosummary/an.build.keys.html.md#an.build.keys.PROJECT_ROOT_FILES), the library lockfile), read by path. A re-pin
changes what is checked out, so it moves every key, conservatively.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> project_dependencies({}) == project_dependencies({"props": {"a": 1}})
True
```

### an.build.keys.project_root_files_digest(project_root, , files=('assets.lock.json',))

`{name: sha256 or ABSENT}` for the project-root files every shot depends on.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     project_root_files_digest(d, files=["assets.lock.json"])
{'assets.lock.json': 'absent'}
```

### an.build.keys.register_shot_key_part(renderer_name, part_name, part)

Add one named input to every key of `renderer_name`’s shots — additively.

For an input the render reads OUTSIDE its compiled document (whatever
changes the document is already covered by the `compiled` part, with
early cutoff for free): a genre package’s side file, a vocabulary entry’s
version (P7). Refuses a duplicate `part_name`; a name that collides with
one of the keyer’s own parts is refused when the key is computed.

A renderer registered LAZILY (the stage, an#247) brings its keyer when the
renderer registry first loads, so this loads it first – a genre adding a
key part at install (`cutan`, P8) needs no prior lookup.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.build.keys.register_shot_keyer(renderer_name, keyer, , environment=None, renderer_type=None, records_reads=False, replace=False)

Declare how shots of `renderer_name` are keyed, and how its machine is probed.

The registration seam for every backend (cut-out here; Manim’s opaque
shots, keyed on source hash + Manim version + quality, are the next).
`renderer_type` binds the keyer to one renderer class: a renderer whose
type is not exactly it is never cached. `records_reads=True` claims
that every asset the shot depends on is read through `ctx.mall` (or is
already digested by one of the keyer’s parts): the engine then keys the
shot on the entries it read rather than on the whole project. A second registration for a name
is refused unless `replace=True` — a silent replacement would drop the
first keyer’s parts from every key without anyone saying so.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.build.keys.registered_shot_keyers()

The renderer names that have a keyer.

A keyer registers beside its renderer, and a backend behind the import
firewall registers when the renderer registry first loads it (an#247), so
this loads the registry first.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> "cutout" in registered_shot_keyers()
True
```

### an.build.keys.shot_keyer_for(renderer)

The keyer that describes `renderer` (an instance, or a name), or `None`.

`None` means the shot is never cached: no keyer for the name, or a
renderer whose class is not the one the keyer was registered for.

* **Return type:**
  `_KeyerEntry` | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.build.keys.store_digest(store)

A digest of one store’s whole content.

A filesystem store (one exposing `_root`) is digested from its FILES,
sidecars included — a character’s `meta.json` is not its art; the SVG
parts beside it are. That reads behind the mapping, which is ADR 0004’s gap
3 (“art bypasses the mall”), and it is exactly why this is the fallback:
once art is read through the stores (the asset library, ADR 0005), a
read-recording view replaces it with the keys a shot actually read. Any
other mapping is digested from its items.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> store_digest({"a": {"x": 1}}) == store_digest({"a": {"x": 1}})
True
>>> store_digest({"a": {"x": 1}}) == store_digest({"a": {"x": 2}})
False
```


# _autosummary/an.build.reads.html.md

# an.build.reads

What a shot read: a read-recording view of the mall, and the digests of what it saw.

ADR 0004 decision 3: *a shot’s inputs are recorded, not assumed.* The mall a
keyer compiles against is wrapped in a [`RecordingMall`](_autosummary/an.build.reads.html.md#an.build.reads.RecordingMall), which notes
every asset entry the compile asks for — `props["logo"]`, a `get`, an
`in` test — and every store it LISTS (iterating a store, or taking its
length, depends on all of it). Those reads become the shot’s dependency edges:
[`read_digests()`](_autosummary/an.build.reads.html.md#an.build.reads.read_digests) digests exactly them, and an edit to an asset no shot
read moves no key. Before an#316 every shot depended on every asset in its
project ([`an.build.keys.project_assets_digest()`](_autosummary/an.build.keys.html.md#an.build.keys.project_assets_digest), the first slice’s
fallback), so a one-prop edit re-rendered the whole film.

Only the asset stores ([`PROJECT_ASSET_STORES`](_autosummary/an.build.keys.html.md#an.build.keys.PROJECT_ASSET_STORES)) are
recorded. Everything else a render reads already has a part of its own: the
scene reaches the key through each shot’s compiled document, the dialogue
audio through `audio`, art staged by path through `textures`.

An entry’s digest covers what the mapping returns AND, for a filesystem store,
the files of the entry beside it (a prop’s `prop.json`, its `parts/` SVGs,
a relative font) — so a sidecar edited behind the mapping still moves it.

```pycon
>>> mall = RecordingMall({"props": {"logo": {"text": "A"}, "intro": {}}})
>>> mall["props"]["logo"]["text"]
'A'
>>> sorted(mall.reads)
[('props', 'logo')]
>>> sorted(read_digests({"props": {"logo": {"text": "A"}}}, mall.reads))
['props/logo']
```

### Module Attributes

| [`WHOLE_STORE`](_autosummary/an.build.reads.html.md#an.build.reads.WHOLE_STORE)   | The key a read of a WHOLE store is recorded under (iteration, `len`): the shot depends on every entry, present and future.   |
|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|

### Functions

| [`entry_digest`](_autosummary/an.build.reads.html.md#an.build.reads.entry_digest)(store, key)   | One entry's digest: what the mapping returns, plus its files on disk.          |
|-----------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`entry_files`](_autosummary/an.build.reads.html.md#an.build.reads.entry_files)(store, key)    | The files of entry `key` in a filesystem store (one exposing `_root`).         |
| [`read_digests`](_autosummary/an.build.reads.html.md#an.build.reads.read_digests)(mall, reads)  | `{"store/key": digest}` for every recorded read — the shot's dependency edges. |

### Classes

| [`RecordingMall`](_autosummary/an.build.reads.html.md#an.build.reads.RecordingMall)(mall, \*[, recorded])   | A view of `mall` that records which asset entries are read through it.                                               |
|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| [`RecordingStore`](_autosummary/an.build.reads.html.md#an.build.reads.RecordingStore)(store, name, reads)    | One store of a [`RecordingMall`](_autosummary/an.build.reads.html.md#an.build.reads.RecordingMall): reads are noted, writes pass through. |

### *class* an.build.reads.RecordingMall(mall, , recorded=('characters', 'environments', 'props', 'styles', 'voices', 'sounds'))

Bases: [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)

A view of `mall` that records which asset entries are read through it.

Stores named in `recorded` come back wrapped in a [`RecordingStore`](_autosummary/an.build.reads.html.md#an.build.reads.RecordingStore)
(one per store, so identity is stable across lookups); every other store
comes back as it is. `reads` is the set of `(store, key)` read so
far, `key` being [`WHOLE_STORE`](_autosummary/an.build.reads.html.md#an.build.reads.WHOLE_STORE) for a store that was listed.

### *class* an.build.reads.RecordingStore(store, name, reads)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

One store of a [`RecordingMall`](_autosummary/an.build.reads.html.md#an.build.reads.RecordingMall): reads are noted, writes pass through.

Attribute access (`_root`, a store’s own helpers) reaches the wrapped
store unchanged — the compiler reads art by path through `_root`, and
those bytes are keyed by the `textures` part, not here. `bool()` is
NOT a read: `mall.get(name) or {}` asks whether the store has anything,
and recording it would make every shot depend on the whole store. So a
read of an EMPTY store (which that idiom replaces with `{}`) goes
unrecorded — safe, because what the shot draws from an entry that appears
later reaches its compiled document.

### an.build.reads.WHOLE_STORE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '\*'*

The key a read of a WHOLE store is recorded under (iteration, `len`):
the shot depends on every entry, present and future.

### an.build.reads.entry_digest(store, key)

One entry’s digest: what the mapping returns, plus its files on disk.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> entry_digest({"a": 1}, "a") == entry_digest({"a": 1}, "a")
True
>>> entry_digest({"a": 1}, "a") == entry_digest({"a": 2}, "a")
False
>>> entry_digest({}, "a") == entry_digest({}, "a")  # absent is a value too
True
```

### an.build.reads.entry_files(store, key)

The files of entry `key` in a filesystem store (one exposing `_root`).

Both store layouts are covered: a sidecar store’s folder `<root>/<key>/`
(everything under it) and a file store’s `<root>/<key>.<ext>` (one
suffix: `logo.json` is entry `logo`’s; `logo.v2/` and
`logo.v2.json` are entry `logo.v2`’s). Operating-system clutter is
never an asset (`an.stores._common.is_os_junk()`).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### an.build.reads.read_digests(mall, reads)

`{"store/key": digest}` for every recorded read — the shot’s dependency edges.

A read of a store the mall does not have is recorded as `ABSENT`, so
the day the store appears the key moves.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> read_digests({}, [("props", "logo")])
{'props/logo': 'absent'}
```


# _autosummary/an.build.shot_cache.html.md

# an.build.shot_cache

The content-keyed shot cache: ADR 0004’s first slice, behind the `incremental=` seam.

`render_project` asks an *incremental engine*, per shot, for a plan: the
shot’s key, and — when an entry with that key exists — the rendered shot to
reuse. It renders only the misses, and hands each fresh render back to be
recorded. The engine is a seam (`incremental=` on `an.render.render`), and
[`ShotCache`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotCache) is its built-in default; an `nw`-backed engine is the
planned alternative (ADR 0004 decision 5).

\*\*Data model: `lacing`’s\*\* (decision 4). An entry is a `ShotArtifact` —
a `lacing.Artifact` (`asset_id` = sha256 of the mp4, W3C-PROV provenance
whose `was_derived_from` lists the key’s input digests) plus the named parts
of its key and the timings that produced it. Entries live in a
`lacing.ArtifactStore`: a catalog (`key -> record`) and a content-addressed
blob store, both injected `dol` mappings. In a project the store is
`mall["shot_cache"]` (`artifacts/shot_cache/{catalog,blobs}/`).

An ASSEMBLED film (transitions, a sound layer) is a stream-copy concat of
segments (`an.assemble`, an#260): a shot no transition touches needs only its
mp4, and a shot one touches also needs its *parts* — its body, encoded once,
and the PNGs inside its transition window — a second entry,
`<key>.parts.<code>.h<head>.t<tail>`, of a few MB. So such a film reuses its
shots by default. (The older whole-frames entry, `<key>.frames`, is read only
by a caller that asks for it with `needs_frames=True`; the render loop no
longer does.)

Each cached render also records a *root* (`root.<digest>`): which entries a
render of output `<name>` under one set of render knobs on one machine used.
Roots are what `an.build.gc` keeps alive, beside what the current scene reaches.

**Invalidation is by digest, never by deletion** (decision 6): a changed input
is a different key, and the old entry simply stops being asked for. The
pre-cache `artifacts/shots/<shot.id>.mp4` archive is not read. Collecting
unreachable entries is a separate, explicit command: `an cache gc`
([`an.build.gc`](_autosummary/an.build.gc.html.md#module-an.build.gc)).

```pycon
>>> from an.build.shot_cache import BuildReport, ShotOutcome
>>> r = BuildReport([ShotOutcome("a", "cutout", "reused", key="k" * 64),
...                  ShotOutcome("b", "cutout", "rendered", key="j" * 64, reason="new or changed")])
>>> r.summary()
'2 shot(s): 1 rendered (b), 1 reused (a); not reused: new or changed (b)'
```

### Module Attributes

| [`SHOT_CACHE_STORE`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.SHOT_CACHE_STORE)   | The mall key of the shot cache.                                                                                          |
|---------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| [`PARTS_INFIX`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.PARTS_INFIX)        | what an assembled film takes from a shot its transitions touch — its body, encoded once, and the PNGs inside its window. |
| [`ROOT_PREFIX`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ROOT_PREFIX)        | what one render used (see [`ShotCache.record_root()`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotCache.record_root)).                     |

### Functions

| [`default_environment_digest`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.default_environment_digest)(renderer_name)        | The digest of `renderer_name`'s registered environment probe, once per process.                                                                  |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| [`explain_change`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.explain_change)(before, before_reads, after, ...) | Why a shot keyed `after` is not the entry keyed `before`, in words.                                                                              |
| [`human_bytes`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.human_bytes)(n)                                   | `n` bytes for a person: 1024-based, one decimal.                                                                                                 |
| [`in_memory_shot_cache_store`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.in_memory_shot_cache_store)()                     | A shot cache held in dicts — for tests, and for a mall with no disk.                                                                             |
| [`machine_id`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.machine_id)()                                     | A short digest naming this machine (its host name and hardware address), so each machine's renders of a synced project keep a root of their own. |
| [`project_id`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.project_id)(project_root)                         | A short digest naming a project by its resolved directory, so two projects sharing one cache store keep a root each (`""` for none).             |
| [`parts_entry_id`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.parts_entry_id)(key, window)                      | The catalog id of the parts entry of shot `key` for `window`.                                                                                    |
| [`resolve_incremental`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.resolve_incremental)(incremental)                 | `incremental=` → an engine, or `None` for "render every shot cold".                                                                              |
| [`shot_artifact_type`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.shot_artifact_type)()                             | The record type (`lacing.Artifact` subclass), built on first use.                                                                                |
| [`shot_cache_store`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.shot_cache_store)(root)                           | A filesystem shot cache under `root`: `catalog/` + `blobs/` (lacing's layout).                                                                   |
| [`store_usage`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.store_usage)(store)                               | `(bytes, entries)` of a shot cache store: every blob byte on disk (or, off disk, every distinct blob its records name) and its catalog entries.  |

### Classes

| [`BuildReport`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.BuildReport)([outcomes, store_bytes, ...])       | Every shot's outcome, in timeline order.                                      |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`IncrementalEngine`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.IncrementalEngine)(\*args, \*\*kwargs)           | The `incremental=` seam of `an.render.render` (ADR 0004 decision 5).          |
| [`ShotCache`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotCache)([store, environment, ...])            | The built-in engine: look a shot's key up in an ArtifactStore; record misses. |
| [`ShotOutcome`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotOutcome)(shot_id, renderer, status[, ...])   | What happened to one shot in one render, with its wall times (seconds).       |
| [`ShotPlan`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotPlan)(shot_id, renderer, key[, inputs, ...]) | The engine's answer for one shot: its key, and what to reuse if anything.     |

### Exceptions

| [`ShotCacheWarning`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotCacheWarning)   | The shot cache could not do something it should have; the render still stands.   |
|---------------------------------------------------------------------|----------------------------------------------------------------------------------|

### *class* an.build.shot_cache.BuildReport(outcomes=<factory>, store_bytes=None, store_entries=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Every shot’s outcome, in timeline order.

#### store_bytes *: [int](https://docs.python.org/3/builtins/functions.html#int) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

The shot cache’s size after the render, when the engine measured it.

#### store_line()

How big the shot cache is after the render, or `""` if unmeasured.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> BuildReport(store_bytes=3 * 1024**2, store_entries=4).store_line()
'3.0 MB in 4 entries'
```

#### summary()

One line: what was rendered, what reused, and WHY each rendered shot
was not reused — a cache that silently re-renders everything reads as
a broken cache (an#243 review, R2-1).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> BuildReport([ShotOutcome("a", "cutout", "uncached", reason=FRAMES_NOT_CACHED)]).summary()
'1 shot(s): 1 rendered (a), 0 reused (-); not reused: frames not cached: film has transitions/sound; pass --cache-frames (a)'
```

#### timing_table()

A Markdown table of the per-shot wall times.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### *class* an.build.shot_cache.IncrementalEngine(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

The `incremental=` seam of `an.render.render` (ADR 0004 decision 5).

`begin` once per render with the project mall (and its root, for the
root files every shot depends on); `plan` once per shot,
BEFORE any shot renders (in the calling thread); `record` once per shot
that was rendered (possibly from a worker thread); `finish` returns the
report.

Two hooks are OPTIONAL (the render loop calls them when an engine has
them): `record_parts(plan, parts)` once per rendered shot whose film
window is not whole (an#260), and `record_root(output_name, profile=...,
output=...)` once the film is delivered (what the garbage collector keeps).

### an.build.shot_cache.PARTS_INFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '.parts.'*

what an assembled
film takes from a shot its transitions touch — its body, encoded once, and
the PNGs inside its window. See [`parts_entry_id()`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.parts_entry_id).

* **Type:**
  The catalog id infix of a shot’s PARTS entry (an#260)

### an.build.shot_cache.ROOT_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'root.'*

what one render used (see [`ShotCache.record_root()`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotCache.record_root)).

* **Type:**
  The catalog id prefix of a ROOT

### an.build.shot_cache.SHOT_CACHE_STORE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'shot_cache'*

The mall key of the shot cache.

### *class* an.build.shot_cache.ShotCache(store=None, \*, environment=<function default_environment_digest>, dependencies=<function project_dependencies>, fallback=<function every_asset_digest>, record_reads=True, cache_frames=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The built-in engine: look a shot’s key up in an ArtifactStore; record misses.

`store` is the injected `lacing.ArtifactStore`; `None` means the
mall’s `shot_cache` (resolved in `begin()`), and a mall without one
renders every shot. `environment(renderer_name) -> digest` is the
environment seam — injectable so a test (or a remote-render backend) can
state its machine rather than probe this one. Three dependency seams (see
`Dependencies`):

- `record_reads` (on by default) keys a shot whose keyer is registered
  with `records_reads=True` on the asset entries its compile read
  ([`an.build.reads`](_autosummary/an.build.reads.html.md#module-an.build.reads), an#316: the `assets` part), so an edit to an
  asset re-renders only the shots that read it;
- `fallback` is the `assets` part of every OTHER shot — a keyer that
  cannot vouch for its reads, or every shot under `record_reads=False`:
  by default every asset in the project (decision 3’s first slice);
- `dependencies` is what EVERY shot depends on (the `project` part): by
  default the library lockfile. `None` drops it, and `fallback=None`
  keys an unrecorded shot on its own parts alone — use either knowingly.

An engine built with other seams writes keys `an cache gc` (which
recomputes the current scene’s keys with the defaults) does not reach:
collect such a cache with the same engine.
`cache_frames` also stores each shot’s whole PNG sequence when a caller
plans with `needs_frames=True`. The render loop no longer does (an#260):
an assembled film takes a shot’s mp4 and, at a transition, its *parts*
(`window=`), which are cached by default at a few MB. Kept for callers of
the engine that need every frame; `an cache gc` collects old frames.

After a render, `report` holds what happened to each shot.

#### entry_ids(shot, renderer, ctx, , window=None)

The catalog ids a [`plan()`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotCache.plan) of this shot would read — computed by
the same code, looking nothing up and rendering nothing. What
`an.build.gc` keeps for the current scene. Call `begin()` first.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### plan(shot, renderer, ctx, , needs_frames=False, window=None, force=False)

`window` is what an assembled film needs from this shot
(`an.assemble.ShotWindow`; `None` for a plain concat): a shot whose
window is not whole is reused only when its parts entry is there too.

* **Return type:**
  [`ShotPlan`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotPlan)

#### record_parts(plan, parts)

Store a freshly rendered shot’s `an.assemble.ShotParts` under
`plan.parts_id`: its body mp4 and its window’s PNGs, one stored zip.
A failed write never fails the render (the next render renders it).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### record_root(output_name, , profile, output)

Record what this render used, as a ROOT for `an.build.gc`; return its id.

A root is keyed by the project ([`project_id()`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.project_id)), the output name,
the render `profile` (the knobs as passed — `None` meaning “the
scene’s own”) and this machine ([`machine_id()`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.machine_id)), so the next render
of the same output with the same knobs on the same machine replaces it
— a browser upgrade included — while a render on another machine of a
synced project, or of another project sharing the store, keeps its own. Its record is a `lacing.Artifact` of the delivered film,
derived from the shot keys. Also measures the store, for the summary.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *exception* an.build.shot_cache.ShotCacheWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

The shot cache could not do something it should have; the render still stands.

### *class* an.build.shot_cache.ShotOutcome(shot_id, renderer, status, key=None, reason='', key_s=None, compile_s=None, render_s=None, cached_render_s=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What happened to one shot in one render, with its wall times (seconds).

`key_s` is the whole key computation, of which `compile_s` is the
compile; `render_s` is this render’s wall time (`None` when reused) and
`cached_render_s` the wall time of the render being reused.

### *class* an.build.shot_cache.ShotPlan(shot_id, renderer, key, inputs=<factory>, reads=<factory>, code=<factory>, cached=None, reason='', key_s=None, compile_s=None, cached_render_s=None, needs_frames=False, window=None, parts_id=None, parts=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The engine’s answer for one shot: its key, and what to reuse if anything.

#### code *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

The render path’s modules, digested (`{module: digest}`, an#395).

#### parts *: [Any](https://docs.python.org/3/library/typing.html#typing.Any)* *= None*

The reused parts (`an.assemble.ShotParts`), materialised for this plan.

#### parts_id *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

The catalog id of this shot’s parts entry, when its window is not whole.

#### reads *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

The asset entries the shot read, digested (`{"store/key": digest}`).

#### window *: [Any](https://docs.python.org/3/library/typing.html#typing.Any)* *= None*

What an assembled film needs from this shot (`an.assemble.ShotWindow`);
`None` for a film that is a plain concat of shot mp4s.

### an.build.shot_cache.default_environment_digest(renderer_name)

The digest of `renderer_name`’s registered environment probe, once per process.

Memoised per renderer NAME (each backend has its own machine: Chromium and
ffmpeg for cut-out, a Manim install for Manim), and once per process,
because the cut-out probe launches a browser and encodes a frame. So a
long-lived host does not see a `playwright install` or a `brew upgrade`
made after its first render: restart it, or pass `force_render`. A
renderer with no probe has the empty environment.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.build.shot_cache.explain_change(before, before_reads, after, after_reads, , before_code=None, after_code=None)

Why a shot keyed `after` is not the entry keyed `before`, in words.

The assets it read that moved come first, by name (`asset changed:
props/logo`), then every other key part that moved, appeared or went, by
`PART_LABELS` (an optional part — `fonts`, `runtime_extensions` —
comes and goes with what the shot draws). The parts that follow from an
asset (`FOLLOWS_ASSETS`) are left out when one is named. Both keys
are of one composition: a different `key_version` is
`KEY_FORMAT_CHANGED`, decided before this is called.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> explain_change({"compiled": "a", "assets": "x"}, {"props/logo": "1"},
...                {"compiled": "b", "assets": "y"}, {"props/logo": "2"})
'asset changed: props/logo'
>>> explain_change({"knobs": "a", "compiled": "c"}, {}, {"knobs": "b", "compiled": "c"}, {})
'render settings changed'
>>> explain_change({"compiled": "a"}, {}, {"compiled": "a", "fonts": "f"}, {})
'the system fonts changed'
>>> explain_change({"assets": "a"}, {}, {"assets": "b"}, {})  # an unrecorded shot
'a project asset changed'
>>> explain_change({"code": "a"}, {}, {"code": "b"}, {},
...                before_code={"an.motion": "1"}, after_code={"an.motion": "2"})
"an's render code (an.motion) changed"
```

### an.build.shot_cache.human_bytes(n)

`n` bytes for a person: 1024-based, one decimal.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> human_bytes(0), human_bytes(1536), human_bytes(5 * 1024**3)
('0 B', '1.5 KB', '5.0 GB')
```

### an.build.shot_cache.in_memory_shot_cache_store()

A shot cache held in dicts — for tests, and for a mall with no disk.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.build.shot_cache.machine_id()

A short digest naming this machine (its host name and hardware
address), so each machine’s renders of a synced project keep a root of
their own. A digest, never the names themselves.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> len(machine_id())
16
```

### an.build.shot_cache.parts_entry_id(key, window)

The catalog id of the parts entry of shot `key` for `window`.

The id names the window (the same shot cut for another neighbour is a
different entry) and the digest of the code that cuts and encodes the parts
(`parts_code_digest()`): a parts entry is produced by `an.assemble`, not
by the renderer, so the renderer’s own `code` key part does not cover it.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from an.assemble import ShotWindow
>>> parts_entry_id("k" * 64, ShotWindow(frames=30, head=0, tail=4)).endswith(".h0.t4")
True
```

### an.build.shot_cache.project_id(project_root)

A short digest naming a project by its resolved directory, so two
projects sharing one cache store keep a root each (`""` for none).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> project_id(None)
''
>>> len(project_id("."))
16
```

### an.build.shot_cache.resolve_incremental(incremental)

`incremental=` → an engine, or `None` for “render every shot cold”.

`True` is a fresh [`ShotCache`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.ShotCache) over the mall’s store; `False` or
`None` is off; anything else must be an [`IncrementalEngine`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.IncrementalEngine).

* **Return type:**
  [`IncrementalEngine`](_autosummary/an.build.shot_cache.html.md#an.build.shot_cache.IncrementalEngine) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> resolve_incremental(False) is None
True
>>> isinstance(resolve_incremental(True), ShotCache)
True
```

### an.build.shot_cache.shot_artifact_type()

The record type (`lacing.Artifact` subclass), built on first use.

### an.build.shot_cache.shot_cache_store(root)

A filesystem shot cache under `root`: `catalog/` + `blobs/` (lacing’s layout).

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.build.shot_cache.store_usage(store)

`(bytes, entries)` of a shot cache store: every blob byte on disk (or,
off disk, every distinct blob its records name) and its catalog entries.

On a filesystem store this is a directory listing, not a read of every
record, so the render summary can afford it on every render.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]


# _autosummary/an.capabilities.html.md

# an.capabilities

Capabilities: what an asset, an engine or the environment affords, and the one matcher.

ADR 0002 (capability-based applicability, with a universal default) and the core
study §2.13 (three subjects). This is the LOWER layer of the pair: it knows
capabilities, analysers, requirements and substitution records, and nothing
about vocabulary entries, methods or aspects — [`an.semantic`](_autosummary/an.semantic.html.md#module-an.semantic) imports it,
never the reverse, and neither imports [`an.ir`](_autosummary/an.ir.html.md#module-an.ir) at module level.

- A **capability** is a named, possibly parametrised fact (`limbs.legs`,
  `face.mouth` with chart `rhubarb9`, `env.latex`). Names use one dotted
  grammar, are registered with a description and a **remedy** (what would add
  it, and the command where one exists), and are **persisted identifiers**: the
  asset library’s facets and the substitution records store them. A capability
  belongs to one **subject**: `asset`, `engine` or `environment`.
- An **analyser** derives a subject’s capabilities — a **profile**,
  `{capability: params}` — from what is there (a descriptor and its art, a
  renderer’s implemented members, the tools on `PATH`), never from a list
  typed beside it (ADR 0002 decision 2). It is versioned: a stored snapshot
  made by an older analyser is recomputed ([`current_affordances()`](_autosummary/an.capabilities.html.md#an.capabilities.current_affordances)).
  Declared facts used *instead of* deriving are listed under the
  `overrides` param, so the derivation reports which overrides it used.
- A **requirement** is a predicate over a profile, spelled as a string. The
  whole grammar (consult on P7, §3): `cap` (afforded), `cap:key` (`key`
  among the capability’s `keys`), `cap>=N` (its `count` param — else its
  number of `keys` — is at least `N`) and `a|b` (any of). No callables:
  a requirement must name its remedy, render into the generated docs and the
  MCP surface, and diff when a method’s version bumps.
- [`missing()`](_autosummary/an.capabilities.html.md#an.capabilities.missing) is THE matcher: the requirement terms a profile (or a
  [`Subjects`](_autosummary/an.capabilities.html.md#an.capabilities.Subjects) triple) does not meet, in the order asked. `why_not`,
  > `applicable` and `resolve` (in [`an.semantic`](_autosummary/an.semantic.html.md#module-an.semantic)) and the library’s
  > `find(…, near=True)` are all calls to it.
- A [`Substitution`](_autosummary/an.capabilities.html.md#an.capabilities.Substitution) records that a method other than the requested one
  was used (generalising the compiled document’s `asset_resolution`).
  `policy` choices are information; `missing` and `noop` are warnings
  that `--strict-assets` makes fatal ([`FATAL_REASONS`](_autosummary/an.capabilities.html.md#an.capabilities.FATAL_REASONS)).

```pycon
>>> profile = {"swap.view": {"keys": ["front", "side"]}, "limbs.legs": {}}
>>> matches(profile, "swap.view:side"), matches(profile, "swap.view:back"), matches(profile, "limbs.legs")
(True, False, True)
>>> missing(profile, ["limbs.legs", "face.mouth|face.jaw", "swap.view>=3"])
['face.mouth|face.jaw', 'swap.view>=3']
```

### Module Attributes

| [`SUBJECTS`](_autosummary/an.capabilities.html.md#an.capabilities.SUBJECTS)        | The three subjects a capability can belong to (core study §2.13).                    |
|------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`KEY_SEP`](_autosummary/an.capabilities.html.md#an.capabilities.KEY_SEP)         | Separates a capability from one of its keys in a term (`swap.view:side`).            |
| [`KEYS_PARAM`](_autosummary/an.capabilities.html.md#an.capabilities.KEYS_PARAM)      | The params entry listing the discrete values a capability affords.                   |
| [`OVERRIDES_PARAM`](_autosummary/an.capabilities.html.md#an.capabilities.OVERRIDES_PARAM) | The params entry listing the declared facts the derivation used instead of deriving. |
| [`Profile`](_autosummary/an.capabilities.html.md#an.capabilities.Profile)         | `{capability: params}` — what an analyser derives, and the matcher's only input.     |
| [`CAPABILITIES`](_autosummary/an.capabilities.html.md#an.capabilities.CAPABILITIES)    | Registered capabilities, by name.                                                    |
| [`ANALYSERS`](_autosummary/an.capabilities.html.md#an.capabilities.ANALYSERS)       | Registered analysers, by kind.                                                       |
| [`FATAL_REASONS`](_autosummary/an.capabilities.html.md#an.capabilities.FATAL_REASONS)   | The reasons `--strict-assets` makes fatal (a test pins this set).                    |

### Functions

| [`affordances`](_autosummary/an.capabilities.html.md#an.capabilities.affordances)(asset[, art, kind])                   | `affordances(asset)` (ADR 0002 decision 2): what `asset` affords, derived.                                                |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| [`analyse`](_autosummary/an.capabilities.html.md#an.capabilities.analyse)(kind, doc[, art])                         | `(profile, analysers)` of one subject: its capabilities and the analyser versions used.                                   |
| [`art_in_dir`](_autosummary/an.capabilities.html.md#an.capabilities.art_in_dir)(directory, \*[, exclude])              | The art an asset folder holds, as an analyser reads it: `{relative path: True}`.                                          |
| [`capability_names`](_autosummary/an.capabilities.html.md#an.capabilities.capability_names)(\*[, subject, owner])            | The registered capability names, sorted; filtered by `subject`/`owner`.                                                   |
| [`capability_of`](_autosummary/an.capabilities.html.md#an.capabilities.capability_of)(query)                              | `(capability, key)` of a query term (the library's `find` form).                                                          |
| [`current_affordances`](_autosummary/an.capabilities.html.md#an.capabilities.current_affordances)(kind, doc, art, \*, ...)      | The stored snapshot when its analyser version is current, else a fresh derivation.                                        |
| [`matches`](_autosummary/an.capabilities.html.md#an.capabilities.matches)(profile, term)                            | Whether `profile` (or the subject of `term` in a [`Subjects`](_autosummary/an.capabilities.html.md#an.capabilities.Subjects)) meets `term`. |
| [`missing`](_autosummary/an.capabilities.html.md#an.capabilities.missing)(profile, requires)                        | The requirement terms `profile` does not meet, spelled, in the order asked.                                               |
| [`parse_requirement`](_autosummary/an.capabilities.html.md#an.capabilities.parse_requirement)(spec)                           | A [`Requirement`](_autosummary/an.capabilities.html.md#an.capabilities.Requirement) from its spelling (`cap`, `cap:key`, `cap>=N`, `a|b`).     |
| [`register_analyser`](_autosummary/an.capabilities.html.md#an.capabilities.register_analyser)(kind, \*[, version, ...])       | Register an analyser.                                                                                                     |
| [`register_capability`](_autosummary/an.capabilities.html.md#an.capabilities.register_capability)(name, \*[, description, ...]) | Register a capability (or a [`Capability`](_autosummary/an.capabilities.html.md#an.capabilities.Capability)).                                 |
| [`remedy_for`](_autosummary/an.capabilities.html.md#an.capabilities.remedy_for)(term)                                  | What would add the capability a requirement term asks for.                                                                |
| [`subject_of`](_autosummary/an.capabilities.html.md#an.capabilities.subject_of)(term)                                  | The subject whose profile a requirement term is matched against.                                                          |

### Classes

| [`Analyser`](_autosummary/an.capabilities.html.md#an.capabilities.Analyser)(kind, version, derive[, subject, ...])   | The derivation of one kind's profile, versioned.                                   |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| [`Capability`](_autosummary/an.capabilities.html.md#an.capabilities.Capability)(name, description, remedy[, ...])      | A registered capability: its name, what it means, and how to add it.               |
| [`Requirement`](_autosummary/an.capabilities.html.md#an.capabilities.Requirement)([capability, key, at_least, any_of])  | One requirement term: a capability, optionally a key or a count, or a disjunction. |
| [`Subjects`](_autosummary/an.capabilities.html.md#an.capabilities.Subjects)([asset, engine, environment])            | The three profiles a requirement can be matched against.                           |
| [`Substitution`](_autosummary/an.capabilities.html.md#an.capabilities.Substitution)(aspect, entity, requested, ...)      | One recorded substitution: the method asked for, the one used, and why.            |

### Exceptions

| [`CapabilityError`](_autosummary/an.capabilities.html.md#an.capabilities.CapabilityError)   | A capability, analyser or requirement is malformed, or collides with one registered.   |
|--------------------------------------------------------------------|----------------------------------------------------------------------------------------|

### an.capabilities.ANALYSERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Analyser](_autosummary/an.capabilities.html.md#an.capabilities.Analyser)]* *= {'engine': Analyser(kind='engine', version='3', subject='engine', declares=()), 'environment': Analyser(kind='environment', version='2', subject='environment', declares=())}*

Registered analysers, by kind.

### *class* an.capabilities.Analyser(kind, version, derive, subject='asset', declares=(), overrides=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The derivation of one kind’s profile, versioned.

`kind` is an asset kind (`character`) or a subject that has one
analyser (`engine`, `environment`). Bump `version` whenever the
output can change for the same input: snapshots made under the old one are
then recomputed on read. The version is NOT a compile input (consult §5):
the derived profile is, so touching an analyser without changing its output
re-renders nothing.

#### declares *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ()*

The document’s declared facts the derivation honours instead of deriving
(`rest_view`, `face_overlay`) or reads as a request (`gait`):
reported by `describe_asset` (ADR 0002 decision 2).

#### overrides *: [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]], [Iterable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]] | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

`(doc, art) -> declared field names` the derivation USED as overrides
that do not show on an afforded capability’s params: a declared fact
that REMOVES a capability (a character’s `occluded`, an#381). Those on
an afforded capability are already in its `overrides` param.
`None`: none beyond those. Reporting only: it changes no profile, so
adding it does not bump `version`.

### an.capabilities.CAPABILITIES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Capability](_autosummary/an.capabilities.html.md#an.capabilities.Capability)]* *= {'engine.compile': Capability(name='engine.compile', description='the engine implements the optional \`compile\` member', remedy='use an engine that implements \`compile\`', subject='engine', command=None, version='1'), 'engine.measure_duration': Capability(name='engine.measure_duration', description='the engine implements the optional \`measure_duration\` member', remedy='use an engine that implements \`measure_duration\`', subject='engine', command=None, version='1'), 'engine.paint_order': Capability(name='engine.paint_order', description="the engine paints parts in an order that is not their tree's (keys: \`container\`, reorder one container's items; \`global\`, one paint list across containers)", remedy="render the shot with an engine that declares that paint order (the stage does), or give the rig a draw order its tree already paints (\`nesting: flat\`, or each chain's parts together in draw order)", subject='engine', command=None, version='1'), 'engine.preview': Capability(name='engine.preview', description='the engine implements the optional \`preview\` member', remedy='use an engine that implements \`preview\`', subject='engine', command=None, version='1'), 'engine.render': Capability(name='engine.render', description='the engine renders shots (keys: the Shot.renderer values it claims)', remedy="register a renderer that claims the shot's \`renderer\` (an.adapters.register_renderer)", subject='engine', command=None, version='1'), 'engine.render_frames': Capability(name='engine.render_frames', description='the engine implements the optional \`render_frames\` member', remedy='use an engine that implements \`render_frames\`', subject='engine', command=None, version='1'), 'engine.seek': Capability(name='engine.seek', description='the engine implements the optional \`seek\` member', remedy='use an engine that implements \`seek\`', subject='engine', command=None, version='1'), 'env.browser': Capability(name='env.browser', description='Playwright with a Chromium build, which the stage engine renders in', remedy="pip install 'an[cutout]' && playwright install chromium", subject='environment', command=None, version='1'), 'env.ffmpeg': Capability(name='env.ffmpeg', description='\`ffmpeg\` is on PATH', remedy='install ffmpeg (\`brew install ffmpeg\` on macOS, \`apt install ffmpeg\` on Debian)', subject='environment', command=None, version='1'), 'env.key.anthropic': Capability(name='env.key.anthropic', description='the ANTHROPIC_API_KEY environment variable is set (its value is never read)', remedy='set ANTHROPIC_API_KEY (needed by \`an iterate\` and the vision verifier)', subject='environment', command=None, version='1'), 'env.key.elevenlabs': Capability(name='env.key.elevenlabs', description='the ELEVEN_API_KEY environment variable is set (its value is never read)', remedy='set ELEVEN_API_KEY (needed by the ElevenLabs voices)', subject='environment', command=None, version='1'), 'env.latex': Capability(name='env.latex', description='\`latex\` is on PATH', remedy='install a TeX distribution (MacTeX / TeX Live) so \`latex\` is on PATH', subject='environment', command=None, version='1'), 'env.manim': Capability(name='env.manim', description='the manim, manimkit Python package(s) are importable', remedy="pip install 'an[manim]' (Manim Community Edition and manimkit; on Linux first \`apt install libcairo2-dev libpango1.0-dev\`)", subject='environment', command=None, version='1'), 'env.node': Capability(name='env.node', description='\`node\` is on PATH', remedy='install Node.js (\`brew install node\`)', subject='environment', command=None, version='1'), 'env.rhubarb': Capability(name='env.rhubarb', description='\`rhubarb\` is on PATH', remedy='install Rhubarb Lip Sync (\`brew install rhubarb-lipsync\`)', subject='environment', command=None, version='1'), 'rig.hierarchy': Capability(name='rig.hierarchy', description="the rig nests its parts in chains (\`nesting: bones\`): keys = the chain roots, count = the deepest chain's length in parts", remedy="declare \`nesting: bones\` on the rig document and parent each part's bone to the bone it hangs from (an elbow's to the shoulder's)", subject='asset', command=None, version='1'), 'space.framing2d': Capability(name='space.framing2d', description='the engine lowers moves through the framing2d view space: a 2D framing of a flat picture: position, zoom (log), roll (angle)', remedy='render with an engine that lowers the framing2d view space', subject='engine', command=None, version='1'), 'space.orbit3d': Capability(name='space.orbit3d', description='the engine lowers moves through the orbit3d view space: an orbit camera around a 3D target: azimuth and elevation (angles), distance (log)', remedy='render with an engine that lowers the orbit3d view space', subject='engine', command=None, version='1')}*

Registered capabilities, by name.

### *class* an.capabilities.Capability(name, description, remedy, subject='asset', command=None, version='1')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A registered capability: its name, what it means, and how to add it.

`command` is the CLI that adds it, when one exists (`an character
add-views`); `version` bumps when the *meaning* of the name changes (a
persisted name is never redefined in place).

#### to_json()

The capability as the generated docs and the MCP surface list it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *exception* an.capabilities.CapabilityError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A capability, analyser or requirement is malformed, or collides with one registered.

### an.capabilities.FATAL_REASONS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'missing', 'noop'})*

The reasons `--strict-assets` makes fatal (a test pins this set).

### an.capabilities.KEYS_PARAM *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'keys'*

The params entry listing the discrete values a capability affords.

### an.capabilities.KEY_SEP *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= ':'*

Separates a capability from one of its keys in a term (`swap.view:side`).

### an.capabilities.OVERRIDES_PARAM *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'overrides'*

The params entry listing the declared facts the derivation used instead of deriving.

### an.capabilities.Profile

`{capability: params}` — what an analyser derives, and the matcher’s only input.

alias of [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### *class* an.capabilities.Requirement(capability='', key=None, at_least=None, any_of=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One requirement term: a capability, optionally a key or a count, or a disjunction.

```pycon
>>> str(parse_requirement("swap.view:side")), str(parse_requirement("rig.slots>=2"))
('swap.view:side', 'rig.slots>=2')
>>> [str(r) for r in parse_requirement("face.eyes|face.brows").any_of]
['face.eyes', 'face.brows']
```

#### capabilities()

Every capability name this term mentions.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.capabilities.SUBJECTS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('asset', 'engine', 'environment')*

The three subjects a capability can belong to (core study §2.13).

### *class* an.capabilities.Subjects(asset=<factory>, engine=<factory>, environment=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The three profiles a requirement can be matched against.

A bare profile passed where `Subjects` is expected is the asset’s.

```pycon
>>> Subjects.of({"limbs.legs": {}}).asset
{'limbs.legs': {}}
```

### *class* an.capabilities.Substitution(aspect, entity, requested, chosen, reason, requested_version=None, chosen_version='', missing=(), remedies=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One recorded substitution: the method asked for, the one used, and why.

It generalises the compiled document’s `asset_resolution` (ADR 0002
decision 6): a requested method replaced by a default is said out loud,
and fatal under `--strict-assets` unless it was a policy choice.

```pycon
>>> s = Substitution("locomotion", "bob", requested="loco.hem_sway", chosen="loco.rock",
...                  reason="missing", missing=("limbs.legs",))
>>> s.fatal, s.sentence()
(True, "bob: locomotion 'loco.hem_sway' does not apply (missing limbs.legs); used 'loco.rock'")
```

#### sentence()

One human sentence saying what happened.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.capabilities.affordances(asset, art=None, , kind='character')

`affordances(asset)` (ADR 0002 decision 2): what `asset` affords, derived.

`asset` is the document (a mapping, or a pydantic model, dumped to JSON
first); `art` the files present, `{relative path: ref}`. The kind’s
registered analyser decides; with none, nothing is afforded.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.capabilities.analyse(kind, doc, art=None)

`(profile, analysers)` of one subject: its capabilities and the analyser versions used.

A kind with no registered analyser affords nothing *derived* and records no
analyser — an honest empty answer, not a guess.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> analyse("no-such-kind", {})
({}, {})
```

### an.capabilities.art_in_dir(directory, , exclude=())

The art an asset folder holds, as an analyser reads it: `{relative path: True}`.

`exclude` drops the document files themselves (`character.json`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

```pycon
>>> import tempfile, pathlib
>>> with tempfile.TemporaryDirectory() as d:
...     _ = (pathlib.Path(d) / "parts").mkdir(); _ = (pathlib.Path(d) / "parts" / "head.svg").write_text("<svg/>", encoding="utf-8")
...     sorted(art_in_dir(d))
['parts/head.svg']
```

### an.capabilities.capability_names(, subject=None, owner=None)

The registered capability names, sorted; filtered by `subject`/`owner`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.capabilities.capability_of(query)

`(capability, key)` of a query term (the library’s `find` form).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

```pycon
>>> capability_of("swap.view:side"), capability_of("limbs.legs")
(('swap.view', 'side'), ('limbs.legs', None))
```

### an.capabilities.current_affordances(kind, doc, art, , stored, stored_analysers)

The stored snapshot when its analyser version is current, else a fresh derivation.

Affordances are derived data, so recomputing them is always safe; trusting a
snapshot made by an older analyser is what would make a facet lie.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.capabilities.matches(profile, term)

Whether `profile` (or the subject of `term` in a [`Subjects`](_autosummary/an.capabilities.html.md#an.capabilities.Subjects)) meets `term`.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### an.capabilities.missing(profile, requires)

The requirement terms `profile` does not meet, spelled, in the order asked.

THE matcher (ADR 0002 decision 4): `why_not`, `applicable`,
`resolve` and the library’s `find(…, near=True)` all call it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> missing({}, ["env.ffmpeg"]), missing(Subjects(environment={"env.ffmpeg": {}}), ["env.ffmpeg"])
(['env.ffmpeg'], [])
```

### an.capabilities.parse_requirement(spec)

A [`Requirement`](_autosummary/an.capabilities.html.md#an.capabilities.Requirement) from its spelling (`cap`, `cap:key`, `cap>=N`, `a|b`).

* **Return type:**
  [`Requirement`](_autosummary/an.capabilities.html.md#an.capabilities.Requirement)

```pycon
>>> parse_requirement("rig.slots>=2").at_least
2
>>> parse_requirement("limbs.legs(hips)")
Traceback (most recent call last):
...
an.capabilities.CapabilityError: requirement 'limbs.legs(hips)': ...
```

### an.capabilities.register_analyser(kind, , version='', subject='asset', owner='an')

Register an analyser. Two forms.

`register_analyser(Analyser(...), owner=...)` registers the object and
returns it (a genre’s `analysers` field goes this way). With a `kind`
string it is a decorator: `@register_analyser("character", version="0.1.0")`
registers the decorated derivation.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.capabilities.register_capability(name, , description='', remedy='', subject='asset', command=None, version='1', owner='an')

Register a capability (or a [`Capability`](_autosummary/an.capabilities.html.md#an.capabilities.Capability)). Returns it.

Re-registering the same definition is a no-op; a different definition
under a name another owner holds raises, because capability names are
persisted and two meanings for one name would make a stored facet lie.

* **Return type:**
  [`Capability`](_autosummary/an.capabilities.html.md#an.capabilities.Capability)

```pycon
>>> cap = register_capability("demo.thing", description="a thing", remedy="add one", owner="demo")
>>> CAPABILITIES["demo.thing"] is cap
True
>>> _ = drop_owner("demo")
```

### an.capabilities.remedy_for(term)

What would add the capability a requirement term asks for.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> remedy_for("no.such.capability")
'no registered capability no.such.capability; see vocabulary() for the known names'
```

### an.capabilities.subject_of(term)

The subject whose profile a requirement term is matched against.

An unregistered capability is matched against the asset (and reported
missing there): [`an.semantic.check_registry()`](_autosummary/an.semantic.html.md#an.semantic.check_registry) is what refuses it.

* **Return type:**
  str

```pycon
>>> subject_of("env.latex") == subject_of("limbs.legs")
False
```

### Modules

| [`subjects`](_autosummary/an.capabilities.subjects.html.md#module-an.capabilities.subjects)   | The core's two non-asset subjects: the engine (a renderer) and the environment.   |
|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|


# _autosummary/an.capabilities.subjects.html.md

# an.capabilities.subjects

The core’s two non-asset subjects: the engine (a renderer) and the environment.

Core study §2.13: a requirement can name what the **engine** draws (which shot
kinds it claims, which optional members it implements — previz’s rule, “a flag
can never disagree with the code”) and what the **environment** has (ffmpeg, a
browser, LaTeX, an API key). Each gets one minimal, real analyser here, owned by
the core and registered on import of [`an.capabilities`](_autosummary/an.capabilities.html.md#module-an.capabilities):

- `engine` — derived from the renderer object’s *implemented members*:
  `engine.render` (`keys`: the `Shot.renderer` values it claims) and one
  `engine.<member>` per optional member it implements. When the `Engine`
  protocol (P3) lands, its optional members join [`ENGINE_OPTIONAL_MEMBERS`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENGINE_OPTIONAL_MEMBERS)
  and nothing else changes. A stub whose `render` always raises still claims
  its renderer here — the protocol cannot tell yet, and saying so is better
  than a declared flag that could lie. `space.<name>`: the view spaces it
  lowers (an#257), from a `view_spaces` member, else
  `DFLT_ENGINE_VIEW_SPACES`.
- `environment` — cheap probes only (`PATH` lookups, an import spec —
  `env.manim` is `manim` and `manimkit` both importable —, the
  Playwright browser cache, the *presence* of API-key variables — never their
  values). No subprocess, no import of the probed package.

Both analysers take their evidence as `doc` so a test can inject it:
`environment_affordances(probe={"which": {...}, "env": {...}})`.

```pycon
>>> profile = environment_affordances(probe={"which": {"ffmpeg"}, "env": {}, "modules": set(), "browsers": False})
>>> sorted(profile)
['env.ffmpeg']
```

### Module Attributes

| [`ENGINE_ANALYSER_VERSION`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENGINE_ANALYSER_VERSION)      | `engine.measure_duration` joined the derivation (an#279).                                                                                          |
|-------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| [`ENVIRONMENT_ANALYSER_VERSION`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENVIRONMENT_ANALYSER_VERSION) | `env.manim` joined the derivation (an#279).                                                                                                        |
| [`ENGINE_OPTIONAL_MEMBERS`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENGINE_OPTIONAL_MEMBERS)      | Optional engine members a renderer may implement, each afforded as `engine.<member>` when it is a callable attribute of the renderer.              |
| [`ENV_TOOLS`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENV_TOOLS)                    | `capability: (executables, remedy)` — afforded when any executable is on PATH.                                                                     |
| [`ENV_KEYS`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENV_KEYS)                     | `capability: (environment variables, remedy)` — afforded when any is set.                                                                          |
| [`ENV_MODULES`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENV_MODULES)                  | `capability: (python modules, remedy)` — afforded when EVERY module is importable (an import spec only: the module is never imported to find out). |

### Functions

| [`engine_affordances`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.engine_affordances)(renderer)         | What a renderer (or a registered renderer's name) affords, from its members.   |
|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`environment_affordances`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.environment_affordances)(\*[, probe]) | What this machine affords: tools on PATH, a browser, API keys set.             |

### an.capabilities.subjects.ENGINE_ANALYSER_VERSION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '3'*

`engine.measure_duration` joined the derivation (an#279).
“3”: `engine.paint_order` joined it (an#430).

* **Type:**
  ”2”

### an.capabilities.subjects.ENGINE_OPTIONAL_MEMBERS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('compile', 'preview', 'render_frames', 'seek', 'measure_duration')*

Optional engine members a renderer may implement, each afforded as
`engine.<member>` when it is a callable attribute of the renderer.
`measure_duration` is how a whole-shot renderer that OWNS ITS CLOCK (Manim:
only its own `play`/`wait` calls decide how long a shot runs) says so —
the core asks it for the length before laying out the film (an#279).

### an.capabilities.subjects.ENVIRONMENT_ANALYSER_VERSION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '2'*

`env.manim` joined the derivation (an#279).

* **Type:**
  ”2”

### an.capabilities.subjects.ENV_KEYS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...], [str](https://docs.python.org/3/builtins/stdtypes.html#str)]]* *= {'env.key.anthropic': (('ANTHROPIC_API_KEY',), 'set ANTHROPIC_API_KEY (needed by \`an iterate\` and the vision verifier)'), 'env.key.elevenlabs': (('ELEVEN_API_KEY', 'ELEVENLABS_API_KEY'), 'set ELEVEN_API_KEY (needed by the ElevenLabs voices)')}*

`capability: (environment variables, remedy)` — afforded when any is set.

### an.capabilities.subjects.ENV_MODULES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...], [str](https://docs.python.org/3/builtins/stdtypes.html#str)]]* *= {'env.manim': (('manim', 'manimkit'), "pip install 'an[manim]' (Manim Community Edition and manimkit; on Linux first \`apt install libcairo2-dev libpango1.0-dev\`)")}*

`capability: (python modules, remedy)` — afforded when EVERY module is
importable (an import spec only: the module is never imported to find out).

### an.capabilities.subjects.ENV_TOOLS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...], [str](https://docs.python.org/3/builtins/stdtypes.html#str)]]* *= {'env.ffmpeg': (('ffmpeg',), 'install ffmpeg (\`brew install ffmpeg\` on macOS, \`apt install ffmpeg\` on Debian)'), 'env.latex': (('latex', 'pdflatex', 'xelatex'), 'install a TeX distribution (MacTeX / TeX Live) so \`latex\` is on PATH'), 'env.node': (('node',), 'install Node.js (\`brew install node\`)'), 'env.rhubarb': (('rhubarb',), 'install Rhubarb Lip Sync (\`brew install rhubarb-lipsync\`)')}*

`capability: (executables, remedy)` — afforded when any executable is on PATH.

### an.capabilities.subjects.engine_affordances(renderer)

What a renderer (or a registered renderer’s name) affords, from its members.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

```pycon
>>> class Fake:
...     supported_renderers = ("toy",)
...     def render(self, shot, ctx): ...
...     def preview(self, shot): ...
>>> engine_affordances(Fake())
{'engine.render': {'keys': ['toy']}, 'engine.preview': {}}
```

### an.capabilities.subjects.environment_affordances(, probe=None)

What this machine affords: tools on PATH, a browser, API keys set.

`probe` injects the evidence (`which`, `env`, `modules`,
`browsers`); `None` probes the real machine (cheap: no subprocess).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]


# _autosummary/an.captions.html.md

# an.captions

Captions from the word timings the audio pipeline already computes (an#175).

Epic #9 Wave 8, second slice. The audio pipeline stamps each dialogue line
with the provider’s word timings (`Dialogue.word_timings`, line-relative,
an#96) for lip-sync; this module turns them into captions, opt-in through
`meta.captions` ([`Captions`](_autosummary/an.ir.schema.html.md#an.ir.schema.Captions)).

**One cue list, two outputs.** [`caption_pages()`](_autosummary/an.captions.html.md#an.captions.caption_pages) is the single statement
of what is captioned and on which frames. Both outputs are derived from it:

- the picture — [`captioned_shot()`](_autosummary/an.captions.html.md#an.captions.captioned_shot) adds each page as an OVERLAY text
  block ([`an.stage.text`](_autosummary/an.stage.text.html.md#module-an.stage.text); camera-immune, placed at a title-safe anchor) plus
  ordinary `set` actions that show it for exactly its frames, and
  optionally tint the word being spoken;
- the sidecar — [`caption_cues()`](_autosummary/an.captions.html.md#an.captions.caption_cues) places the same pages in FILM time and
  [`dump_srt()`](_autosummary/an.captions.html.md#an.captions.dump_srt) writes SubRip, stored through the `captions` store beside
  > the delivered mp4.

So the two cannot disagree: a page’s cue starts at the first frame that shows
it and ends at the first frame that does not. Times are frames, not seconds,
until the last step:

```pycon
>>> from an.ir.schema import Dialogue, SceneIR, Shot, WordTimingIR
>>> line = Dialogue(speaker="a", text="Hello there, friend.", start=0.5, duration=1.5,
...     word_timings=[WordTimingIR(text=w, start=s, end=s + 0.3)
...                   for w, s in (("Hello", 0.0), ("there,", 0.4), ("friend.", 0.8))])
>>> scene = SceneIR(timeline=[Shot(id="s", duration=3.0, dialogue=[line])])
>>> [(p.start, p.end, p.text) for p in caption_pages(scene, fps=10)]
[(5, 20, 'Hello there, friend.')]
```

**Film time follows the delivered timeline.** A cue’s time is its shot’s
first frame in the film ([`an.assemble.film_timeline()`](_autosummary/an.assemble.html.md#an.assemble.film_timeline) — the same
function the assembler lays the picture out with) plus its shot-local frame,
so a dissolve, which overlaps two shots and shortens the film, moves every
later cue earlier by exactly its overlap.

**Timing is materialised into ordinary actions**, the way [`an.stage.text.reveal_units()`](_autosummary/an.stage.text.html.md#an.stage.text.reveal_units)
works: nothing in the compiler or the runtime knows what a caption is. The
caption blocks are added to the shot at RENDER time, never written back to
the scene — the word timings are the audio pipeline’s output, and a caption
baked into `scene.json` would go stale the moment a line is re-voiced.

**When a line has no word timings** (the offline and Rhubarb providers keep
none), its words are spread evenly over the line’s duration and a
[`CaptionTimingWarning`](_autosummary/an.captions.html.md#an.captions.CaptionTimingWarning) says so; `Captions(strict=True)` raises
instead. A line the audio pipeline has not placed (no `start`) cannot be
captioned and is skipped the same loud way.

\*\*The SubRip cue type is a pinned mirror of `mixing.srt``**, not an import
(epic #9, Decision 2, departed from on measurement): ``mixing`’s package
facade is lazy, so importing `mixing.srt` is cheap — but INSTALLING
`mixing` pulls `moviepy`, `opencv-contrib-python`, `scipy` and
`imageio-ffmpeg`, whose wheel ships an ffmpeg binary built with
`--enable-gpl`. A hard dependency would put a GPL binary inside every
`pip install an`, past a licence perimeter that reads declared metadata
(BSD-2) and would never see it. [`Cue`](_autosummary/an.captions.html.md#an.captions.Cue), [`seconds_to_srt_time()`](_autosummary/an.captions.html.md#an.captions.seconds_to_srt_time) and
[`dump_srt()`](_autosummary/an.captions.html.md#an.captions.dump_srt) therefore mirror `mixing.srt` field for field and byte for
byte, and `tests/test_captions.py` pins both against `mixing` whenever it
is importable, and against a literal otherwise; thorwhalen/mixing#54 asks
for the light base install that would let this become an import. WebVTT is `lacing`’s (its
adapter owns the body schema), so it is not written here.

### Module Attributes

| [`CAPTION_ID_PREFIX`](_autosummary/an.captions.html.md#an.captions.CAPTION_ID_PREFIX)   | A caption page's entity id is this plus its index within the shot.                                                                           |
|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
| [`CAPTION_PROP_REF`](_autosummary/an.captions.html.md#an.captions.CAPTION_PROP_REF)    | The props-store key the caption blocks resolve against — a style-only `TextDescriptor` supplied at render time, never stored in the project. |

### Functions

| [`caption_cues`](_autosummary/an.captions.html.md#an.captions.caption_cues)(pages, timeline)                  | `pages` in FILM time on `timeline` (an [`an.assemble.FilmTimeline`](_autosummary/an.assemble.html.md#an.assemble.FilmTimeline)).   |
|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| [`caption_pages`](_autosummary/an.captions.html.md#an.captions.caption_pages)(scene, \*, fps[, captions])      | Every caption page of `scene`, shot by shot, in the order shown.                                                                              |
| [`captioned_shot`](_autosummary/an.captions.html.md#an.captions.captioned_shot)(shot, pages, captions, \*, ...) | `shot` with its caption pages burned in, and the mall to compile it with.                                                                     |
| [`dump_srt`](_autosummary/an.captions.html.md#an.captions.dump_srt)(cues)                                 | Serialize cues to SubRip text, renumbering from 1.                                                                                            |
| [`paginate`](_autosummary/an.captions.html.md#an.captions.paginate)(words, \*, max_chars, max_lines)      | Split `words` into pages of at most `max_lines` wrapped lines, a new page starting after each sentence end.                                   |
| [`seconds_to_srt_time`](_autosummary/an.captions.html.md#an.captions.seconds_to_srt_time)(seconds)                   | `HH:MM:SS,mmm`, milliseconds ROUNDED with carry; negatives clamp to 0.                                                                        |
| [`srt_for_scene`](_autosummary/an.captions.html.md#an.captions.srt_for_scene)(scene, \*, fps[, pages])         | The SubRip sidecar of `scene` rendered at `fps`.                                                                                              |
| [`wrap_words`](_autosummary/an.captions.html.md#an.captions.wrap_words)(words, max_chars)                   | Greedy line breaks at `max_chars` characters (spaces counted); a word longer than a line gets a line of its own and is never split.           |

### Classes

| [`CaptionPage`](_autosummary/an.captions.html.md#an.captions.CaptionPage)(shot, start, end, lines, word_frames)   | One caption as shown: which shot, which frames, which words.   |
|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`Cue`](_autosummary/an.captions.html.md#an.captions.Cue)(index, start, end, text)                        | One SubRip cue — `mixing.srt.Cue`'s fields, in its order.      |

### Exceptions

| [`CaptionError`](_autosummary/an.captions.html.md#an.captions.CaptionError)         | Captions cannot be built as asked.                              |
|-----------------------------------------------------------------------|-----------------------------------------------------------------|
| [`CaptionTimingWarning`](_autosummary/an.captions.html.md#an.captions.CaptionTimingWarning) | A line is captioned on estimated timing, or not at all.         |
| [`CaptionWarning`](_autosummary/an.captions.html.md#an.captions.CaptionWarning)       | Captions were built, but not everything was captioned as asked. |

### an.captions.CAPTION_ID_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'caption_'*

A caption page’s entity id is this plus its index within the shot.

### an.captions.CAPTION_PROP_REF *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an.captions'*

The props-store key the caption blocks resolve against — a style-only
`TextDescriptor` supplied at render time, never stored in the project.

### *exception* an.captions.CaptionError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

Captions cannot be built as asked. Carries the fix.

### *class* an.captions.CaptionPage(shot, start, end, lines, word_frames, speaker=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One caption as shown: which shot, which frames, which words.

`start`/`end` are SHOT-local frames — the first that shows the page and
the first that does not. `lines` are the words per line, broken by
[`wrap_words()`](_autosummary/an.captions.html.md#an.captions.wrap_words); `word_frames[j]` is the frame word `j` (in reading
order) starts being spoken, clamped into the page.

#### *property* text *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

words joined by spaces, lines by
newlines.

* **Type:**
  The page as both outputs write it

#### *property* words *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Every word of the page, in reading order.

### *exception* an.captions.CaptionTimingWarning

Bases: [`CaptionWarning`](_autosummary/an.captions.html.md#an.captions.CaptionWarning)

A line is captioned on estimated timing, or not at all.

### *exception* an.captions.CaptionWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

Captions were built, but not everything was captioned as asked.

### *class* an.captions.Cue(index, start, end, text)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One SubRip cue — `mixing.srt.Cue`’s fields, in its order.

```pycon
>>> Cue(index=1, start=0.5, end=2.0, text="Hi").duration
1.5
```

#### *property* duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Cue duration in seconds (never negative).

### an.captions.caption_cues(pages, timeline)

`pages` in FILM time on `timeline` (an [`an.assemble.FilmTimeline`](_autosummary/an.assemble.html.md#an.assemble.FilmTimeline)).

A cue starts on the film frame that first shows its page and ends on the
first that does not — the same frames the picture shows it on.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Cue`](_autosummary/an.captions.html.md#an.captions.Cue)]

### an.captions.caption_pages(scene, , fps, captions=None)

Every caption page of `scene`, shot by shot, in the order shown.

`captions` defaults to `scene.meta.captions` (and to the defaults when
that is unset). Dialogue and narration are both captioned (though the
audio pipeline does not voice narration yet, so a narration line has no
`start` in a rendered scene and is skipped with a warning). Within a
shot a page is cut off when the next one starts, so two pages are never
drawn over each other at one anchor; ACROSS a transition they can be — a
dissolve blends the tail of one shot, captions included, with the head of
the next, and a fade takes the burned caption through the colour with the
rest of the picture, while the sidecar’s cue is simply on.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`CaptionPage`](_autosummary/an.captions.html.md#an.captions.CaptionPage)]

### an.captions.captioned_shot(shot, pages, captions, , fps, mall, shot_index=None, base_dir=None, resolution=None)

`shot` with its caption pages burned in, and the mall to compile it with.

Each page becomes an overlay text block `caption_<k>` and the `set`
actions that show it for exactly its frames. With `shot_index`, only the
pages of that shot are used, so a caller can pass the scene’s whole list;
without it every page must belong to one shot. The returned mall is
`mall` with the caption style laid over its props store — nothing is
written to the project. `base_dir` resolves a relative caption `font`.

With `resolution`, every page is typeset now, so a caption the face
cannot draw (an em dash in the embedded face) raises a [`CaptionError`](_autosummary/an.captions.html.md#an.captions.CaptionError)
naming the shot and the words — before a browser launches, rather than
from inside a shot’s compile after others have rendered.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Shot`](_autosummary/an.ir.schema.html.md#an.ir.schema.Shot), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.captions.dump_srt(cues)

Serialize cues to SubRip text, renumbering from 1.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> print(dump_srt([Cue(7, 1.0, 2.5, "Hello\nthere")]))
1
00:00:01,000 --> 00:00:02,500
Hello
there
```

### an.captions.paginate(words, , max_chars, max_lines)

Split `words` into pages of at most `max_lines` wrapped lines, a new
page starting after each sentence end. Returns index ranges.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`range`](https://docs.python.org/3/builtins/stdtypes.html#range)]

```pycon
>>> paginate("One two. Three four five six".split(), max_chars=10, max_lines=1)
[range(0, 2), range(2, 4), range(4, 6)]
```

### an.captions.seconds_to_srt_time(seconds)

`HH:MM:SS,mmm`, milliseconds ROUNDED with carry; negatives clamp to 0.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> seconds_to_srt_time(2592.187), seconds_to_srt_time(-3)
('00:43:12,187', '00:00:00,000')
```

### an.captions.srt_for_scene(scene, , fps, pages=None)

The SubRip sidecar of `scene` rendered at `fps`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from an.ir.schema import Dialogue, SceneIR, Shot, Transition, WordTimingIR
>>> def shot(sid, **kw):
...     line = Dialogue(speaker="a", text="Hi.", start=0.2, duration=0.5,
...                     word_timings=[WordTimingIR(text="Hi.", start=0.0, end=0.4)])
...     return Shot(id=sid, duration=2.0, dialogue=[line], **kw)
>>> scene = SceneIR(timeline=[shot("a"), shot("b", transition=Transition(kind="dissolve"))])
>>> print(srt_for_scene(scene, fps=10))   # b starts at 1.5 s: the dissolve's 0.5 s overlap
1
00:00:00,200 --> 00:00:00,700
Hi.

2
00:00:01,700 --> 00:00:02,200
Hi.
```

### an.captions.wrap_words(words, max_chars)

Greedy line breaks at `max_chars` characters (spaces counted); a word
longer than a line gets a line of its own and is never split.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> wrap_words("the quick brown fox jumps".split(), 10)
[['the', 'quick'], ['brown', 'fox'], ['jumps']]
```


# _autosummary/an.conftest.html.md

# an.conftest

Collection rules for the package’s own doctests.

`an` is in `testpaths` so CI’s `--doctest-modules` reaches the package (an#61).
That flag **imports every module it scans**, which makes one module a problem:
`tests/test_doctest_gate.py` asserts that no module under `an/` imports an
undeclared dependency at module level — one would break CI collection (which
installs no optional extras) the same way, and is much easier to catch there than
here. (The one such module, the `nw` declaration of the cut-out genre, moved to
`cutan`, which skips it when `nw` is absent.)


# _autosummary/an.credits.html.md

# an.credits

What a rendered video owes, and to whom.

`an` composes work it did not create into a video its user ships. Recording where
that work came from (`an.ir.assets.AssetSource`) is half the job; the other half
is being able to *produce* the credits, because \*\*a licence recorded and never
displayed is not compliance\*\* — it is a note to oneself.

So this module walks a project’s reachable assets and answers three questions a
user actually has:

- what third-party work is in this video?
- may I ship it at all? (an#211 — material that is all rights reserved, used
  for private study only, is recognised and said LOUDLY: the report opens with
  it, and a render that uses it ends with a warning that it is not
  publishable);
- what must I display, verbatim, to ship it?
- is anything in here unverified?

The last is the one that matters most and is easiest to lose. An asset with no
licence is reported as **UNKNOWN**, never as “nothing owed”: those are different
answers, and collapsing them is exactly how an obligation goes missing.

Public domain (`pd`, `public-domain`, `cc-pdm-1.0`, `cc0-*`) is recognised as
nothing owed, and an environment’s planes may each carry their own `source`,
so a composite stage — a carved plate plus a CC0 prop — credits both. A
character’s (or prop’s) attachments may too (an#220): a figure composed from
parts carved out of several clips credits each clip, part by part.

### Functions

| [`is_factory_stamp`](_autosummary/an.credits.html.md#an.credits.is_factory_stamp)(raw)                       | Whether a source is the character factory's own stamp (an#236, an#251).              |
|----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`is_generated_source`](_autosummary/an.credits.html.md#an.credits.is_generated_source)(raw)                    | Whether a descriptor's source was written by a generator rather than a person.       |
| [`collect_credits`](_autosummary/an.credits.html.md#an.credits.collect_credits)(mall, \*[, only])           | Walk a project mall and gather every recorded `AssetSource`.                         |
| [`credits_for_project`](_autosummary/an.credits.html.md#an.credits.credits_for_project)(project_dir)            | Credits for the project at `project_dir`.                                            |
| [`credits_for_scene`](_autosummary/an.credits.html.md#an.credits.credits_for_scene)(mall, scene)              | Credits for exactly the assets `scene` draws or plays (an#211).                      |
| [`speech_credits`](_autosummary/an.credits.html.md#an.credits.speech_credits)(mall, scene)                 | One entry per voice whose lines a render synthesized with a named provider (an#271). |
| [`warn_if_private_study`](_autosummary/an.credits.html.md#an.credits.warn_if_private_study)(report, \*[, output]) | Warn, loudly, when `report` holds private-study material.                            |

### Classes

| [`CreditsReport`](_autosummary/an.credits.html.md#an.credits.CreditsReport)([entries, conflicts])   | Everything a project owes, split by whether we actually know.   |
|----------------------------------------------------------------------------------------|-----------------------------------------------------------------|

### Exceptions

| [`PrivateStudyWarning`](_autosummary/an.credits.html.md#an.credits.PrivateStudyWarning)   | A render used material that is all rights reserved, private study only.   |
|------------------------------------------------------------------------|---------------------------------------------------------------------------|

### *class* an.credits.CreditsReport(entries=<factory>, conflicts=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything a project owes, split by whether we actually know.

#### *property* commercial *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether a COMMERCIAL video may contain everything here (publishable,
and nothing non-commercial).

#### conflicts *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]]*

rights conflicts a library check-out carried
(an#357) — a statement about some bytes freer than the one that binds
them. Informative: the binding statement is already among the entries.

* **Type:**
  `(asset, conflict)`

#### format()

Human-readable, and honest about what it does not know.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### *property* noncommercial *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[CreditEntry]*

fine in a personal or
private video, never in a monetised, sponsored or client one. Flagged,
never blocking.

* **Type:**
  Entries for NON-COMMERCIAL use only (an#373)

#### *property* owed *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[CreditEntry]*

Entries that definitely require an attribution (a non-commercial licence owes one too).

#### *property* private *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[CreditEntry]*

all rights reserved, private
study only (an#211). A video containing any of them is not shippable,
whatever else it credits.

* **Type:**
  Entries that may NOT be published

#### *property* publishable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

`False` when any entry is private-study material.

#### *property* unverified *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[CreditEntry]*

Entries whose licence we could not classify.

Deliberately its own list rather than folded into [`owed`](_autosummary/an.credits.html.md#an.credits.CreditsReport.owed). Folding
them in cries wolf; folding them into “nothing owed” hides a real
obligation. Neither is honest, so they are counted separately — the same
reason `priv`’s upkeep keeps `unavailable` apart from `findings`.

### *exception* an.credits.PrivateStudyWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A render used material that is all rights reserved, private study only.

Raised as a warning at the END of a render (an#211), so the last thing the
author reads about the mp4 is that it must not be published.

### an.credits.collect_credits(mall, , only=None)

Walk a project mall and gather every recorded `AssetSource`.

`only` restricts the walk to those `store/key` names (what a render
used, [`credits_for_scene()`](_autosummary/an.credits.html.md#an.credits.credits_for_scene)) — nothing else is read.

Four stores carry provenance: characters, **props** (an#108),
**environments** (an#110) and **sounds**. Each was added by the PR that gave that store
real art, which is the rule rather than a coincidence — a walk that skips a
store holding third-party plates does not return less information, it
returns an affirmative false statement to exactly the people who need the
opposite. **Sounds** joined with the sound layer (an#163) — a sound is
third-party work more often than any other asset. Styles will join when a
StylePack has art (#112).

Legacy reconstruction runs on characters only: it recovers a DiceBear
record from `metadata.dicebear_*`, which no other store has ever written.

* **Return type:**
  [`CreditsReport`](_autosummary/an.credits.html.md#an.credits.CreditsReport)

### an.credits.credits_for_project(project_dir)

Credits for the project at `project_dir`.

* **Return type:**
  [`CreditsReport`](_autosummary/an.credits.html.md#an.credits.CreditsReport)

### an.credits.credits_for_scene(mall, scene)

Credits for exactly the assets `scene` draws or plays (an#211).

[`collect_credits()`](_autosummary/an.credits.html.md#an.credits.collect_credits) walks the whole project; a render owes only what it
used, and a private-study plate sitting unused in the store must not make
an unrelated render “not publishable”. Kept: every entry under a
`store/ref` some shot’s entity names, and every sound a cue names.

* **Return type:**
  [`CreditsReport`](_autosummary/an.credits.html.md#an.credits.CreditsReport)

### an.credits.is_factory_stamp(raw)

Whether a source is the character factory’s own stamp (an#236, an#251).

The factory stamps every part it draws `cc0` with the part’s digest, and
the descriptor with its `source_svg`’s, so the asset library can tell its
shared parts from carved ones. It is `an`’s own work, not third-party: a
credits report lists what is OWED, and listing fifty generated parts per
character would bury the one carved head.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### an.credits.is_generated_source(raw)

Whether a descriptor’s source was written by a generator rather than a person.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_generated_source({"provider": "dicebear", "license": "cc0-1.0"})
True
>>> is_generated_source({"provider": "a-film", "license": "all-rights-reserved"})
False
```

### an.credits.speech_credits(mall, scene)

One entry per voice whose lines a render synthesized with a named provider (an#271).

Read from what the audio pipeline already keeps — no record of its own:
the scene’s lines that carry an `audio_ref` (stamped when synthesized),
each line’s voice as the pipeline resolves it
([`an.audio.voices.line_voice_id()`](_autosummary/an.audio.voices.html.md#an.audio.voices.line_voice_id)), and that voice’s document in
`mall["voices"]` (its `provider`, `voice_id` and `model_id`). A
voice document may declare its own `source` (the provider’s terms, the
licence the user holds); otherwise the speech is listed UNVERIFIED — the
provider’s terms decide what is owed, and nobody recorded them. The licence
that counts for synthesized speech is a provider-terms code
(`an.ir.assets.PROVIDER_TERMS`: `elevenlabs-paid-plan` is `free`;
`elevenlabs-free-plan` is `noncommercial`: it owes a credit and is
flagged for commercial use, an#373), or any licence `an`
recognises;
it is read as the voice’s provider’s, so another provider’s terms count for
nothing (an#307). A voice
whose document names no provider (the offline default) is not listed:
which provider spoke it is not recorded anywhere.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[`CreditEntry`]

```pycon
>>> from types import SimpleNamespace as NS
>>> line = NS(voice_ref="bob", speaker="bob", audio_ref="k1")
>>> scene = NS(timeline=[NS(dialogue=[line], entities=[])])
>>> mall = {"voices": {"bob": {"provider": "elevenlabs", "voice_id": "TX3",
...                            "model_id": "eleven_v3"}}}
>>> [(e.asset, e.license_class, e.source.extra["model"]) for e in speech_credits(mall, scene)]
[('speech/bob', 'unknown', 'eleven_v3')]
>>> mall["voices"]["bob"]["source"] = {"provider": "elevenlabs", "license": "elevenlabs-paid-plan"}
>>> [e.license_class for e in speech_credits(mall, scene)]
['free']
```

### an.credits.warn_if_private_study(report, , output=None)

Warn, loudly, when `report` holds private-study material.

Returns whether it warned. Called at the end of a render, so the warning is
the last word about the file; `output` names it.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)


# _autosummary/an.data.cutout_runtime.html.md

# an.data.cutout_runtime

Moved to [`an.stage.runtime`](_autosummary/an.stage.runtime.html.md#module-an.stage.runtime) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.data.html.md

# an.data

Bundled non-Python resources (cutout JS runtime, etc.).

### Modules

| [`cutout_runtime`](_autosummary/an.data.cutout_runtime.html.md#module-an.data.cutout_runtime)   | Moved to [`an.stage.runtime`](_autosummary/an.stage.runtime.html.md#module-an.stage.runtime) (an#247); this path is a LIVE alias of it.   |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|


# _autosummary/an.determinism.html.md

# an.determinism

The determinism perimeter: what must stay true for a render to be reproducible.

`an` renders by driving a headless Chromium and screenshotting a WebGL canvas.
Two classes of thing can make that non-reproducible, and they need different
treatment:

**Pinned inputs** — the rasteriser, the browser build, the encoder. Those are
*settings*, and they are pinned unconditionally in
[`an.stage.render`](_autosummary/an.stage.render.html.md#module-an.stage.render) (an#31, an#34). Nothing here.

**Latent randomness** — machinery that is deterministic today by accident.
The vendored PixiJS carries `Math.random`, `Date.now`, `performance.now` and
`requestAnimationFrame` calls, and `NoiseFilter`’s default seed is
`Math.random()`. All of it is dormant because the runtime creates the app with
`autoStart: false` and drives it with explicit `app.render()` calls, and
because nothing attaches a filter. Neither fact is written down anywhere that
would go red if it stopped being true. That is what this module watches.

The division of labour is deliberate: `runtime.js`’s `anDeterminismReport`
**observes** and this module **judges**. So the rule is a pure function of a
plain dict — testable with no browser, no ffmpeg and no render — and changing
the rule is a Python diff rather than a runtime re-stage.

Enforcement is **on by default**, which is a deliberate deviation from the
issue’s `AN_DETERMINISTIC=1` framing and follows the same reasoning an#31
used for the launch flags: a property that only holds when someone remembers to
export a variable is not a property of the system. An assertion nobody runs is
worse than no assertion, because the perimeter reads as guarded. The escape
hatch is the same variable read the other way — `AN_DETERMINISTIC=0`.

```pycon
>>> capture_violations({"page": "/index.html", "stage_filter_count": 0,
...                     "filtered_node_paths": [], "shared_ticker_started": False,
...                     "auto_start": False})
[]
```

### Module Attributes

| [`AN_DETERMINISTIC_ENV_VAR`](_autosummary/an.determinism.html.md#an.determinism.AN_DETERMINISTIC_ENV_VAR)   | Read as an OFF switch, not an on switch — see the module docstring.   |
|-----------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`CAPTURE_PAGE`](_autosummary/an.determinism.html.md#an.determinism.CAPTURE_PAGE)               | The page the capture path must be on.                                 |

### Functions

| [`capture_violations`](_autosummary/an.determinism.html.md#an.determinism.capture_violations)(report, \*[, capture_page])   | Return one sentence per breach of the perimeter; empty means clean.       |
|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`determinism_enforced`](_autosummary/an.determinism.html.md#an.determinism.determinism_enforced)()                           | Whether to refuse a render whose determinism perimeter has been breached. |

### an.determinism.AN_DETERMINISTIC_ENV_VAR *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'AN_DETERMINISTIC'*

Read as an OFF switch, not an on switch — see the module docstring.

### an.determinism.CAPTURE_PAGE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'index.html'*

The page the capture path must be on. `render.py` stages `preview.html`
into every work dir and it carries seven clock calls, so “which page did the
browser actually load” is a real question with a wrong answer available.

### an.determinism.capture_violations(report, , capture_page='index.html')

Return one sentence per breach of the perimeter; empty means clean.

Each sentence says what was observed, why it makes frames non-reproducible,
and what to do about it — because the reader of this message is the person
who just added the thing, and “determinism violation” alone tells them
nothing.

A missing key is reported rather than defaulted: a report that does not
carry a field cannot testify that the field is fine, and silently reading
`report.get("shared_ticker_started", False)` would turn a stale runtime
into a clean bill of health.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> capture_violations({})[0].startswith("the determinism report is missing")
True
```

### an.determinism.determinism_enforced()

Whether to refuse a render whose determinism perimeter has been breached.

True unless [`AN_DETERMINISTIC_ENV_VAR`](_autosummary/an.determinism.html.md#an.determinism.AN_DETERMINISTIC_ENV_VAR) is explicitly falsey.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> import os
>>> os.environ.pop("AN_DETERMINISTIC", None) and None
>>> determinism_enforced()
True
>>> os.environ["AN_DETERMINISTIC"] = "0"
>>> determinism_enforced()
False
>>> del os.environ["AN_DETERMINISTIC"]
```


# _autosummary/an.engines.capture.html.md

# an.engines.capture

The capture loop: drive a session through every frame, resolve, write – for ANY engine.

Moved out of the stage renderer (`an/adapters/cutout/render.py`’s
`_capture_frames` and `_capture_frames_canvas`, an#247) and generalised over
the session members of [`an.engines.protocol`](_autosummary/an.engines.protocol.html.md#module-an.engines.protocol). The algorithms are the ones
the stage shipped, unchanged, because the decoded frames are the contract (the
golden corpus and the canvas equivalence gate compare them):

- **Sequential** (a session without `frames`): per frame, `frame(t)` for each
  instant; a frame of ONE instant at supersample 1 with no `resolve` member is
  written as the engine’s own bytes – nothing decoded, so **off is free** –
  and anything else goes through the resolve.
- **Batched** (a session with `frames(requests)`): the requests go out in frame
  order, samples in the order given, at most `batch` frames and
  `batch_pixels` captured pixels per round trip (a frame whose instants alone
  exceed it is split over round trips and resolved once); the resolve runs on a
  small thread pool while the engine draws the next batch, with BACK-PRESSURE –
  at most `max_inflight` frames (and twice the pixel budget) wait on the pool,
  after which the loop blocks on the oldest before asking for more.

**The resolve runs here, in the frame stage, before a file is written**:
supersampling ([`an.media.supersample`](_autosummary/an.media.supersample.html.md#module-an.media.supersample), an exact block mean) and the open
shutter ([`an.media.shutter`](_autosummary/an.media.shutter.html.md#module-an.media.shutter), an exact temporal mean). Nothing downstream
reads a resolution off a file or averages anything; an ffmpeg `-vf scale` is
refused for the reasons in `an-dev-render-pipeline` §2. A session’s own
`resolve` member replaces the default resolve (the stage’s canvas session
refuses non-opaque pixels there); the default is
[`an.media.shutter.mean_png_bytes()`](_autosummary/an.media.shutter.html.md#an.media.shutter.mean_png_bytes).

Two corruptions are silent unless refused, so both are refused: a dropped or
reordered frame (every file is named by its frame number, and every frame
`0..N-1` must exist on disk before the loop returns), and unbounded buffering
(the bounds above).

\*\*The bench’s `supersample` lever rebinds\*\* [`capture_frames()`](_autosummary/an.engines.capture.html.md#an.engines.capture.capture_frames) \*\*on this
module\*\* to force the product’s own factor, so [`an.engines.frame_stage`](_autosummary/an.engines.frame_stage.html.md#module-an.engines.frame_stage)
calls it as a module attribute, at call time.

### Module Attributes

| [`DEFAULT_BATCH`](_autosummary/an.engines.capture.html.md#an.engines.capture.DEFAULT_BATCH)          | Frames per round trip of a batched session.                                                                                              |
|-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| [`DEFAULT_ENCODE_WORKERS`](_autosummary/an.engines.capture.html.md#an.engines.capture.DEFAULT_ENCODE_WORKERS) | Threads resolving and encoding frames while the engine draws the next batch.                                                             |
| [`DEFAULT_BATCH_PIXELS`](_autosummary/an.engines.capture.html.md#an.engines.capture.DEFAULT_BATCH_PIXELS)   | a supersample counts k² times, an open shutter once per instant): at most this many per round trip, twice this many waiting on the pool. |
| [`DEFAULT_MAX_INFLIGHT`](_autosummary/an.engines.capture.html.md#an.engines.capture.DEFAULT_MAX_INFLIGHT)   | frames handed to the pool and not yet written.                                                                                           |

### Functions

| [`capture_frames`](_autosummary/an.engines.capture.html.md#an.engines.capture.capture_frames)(session, requests, frames_dir, \*)   | Write one PNG per request into `frames_dir`, resolved to the declared size.   |
|------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`default_resolve`](_autosummary/an.engines.capture.html.md#an.engines.capture.default_resolve)(samples, \*, frame, factor, size)   | The core's resolve: spatially by `factor`, then the instants in time.         |

### Exceptions

| [`FrameStageError`](_autosummary/an.engines.capture.html.md#an.engines.capture.FrameStageError)   | The frame stage could not produce a frame directory it can vouch for.   |
|--------------------------------------------------------------------|-------------------------------------------------------------------------|

### an.engines.capture.DEFAULT_BATCH *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 8*

Frames per round trip of a batched session. Measured on the stage at
1920x1080 on an M1 Max: 67 ms/frame one frame per call, 46 at four, 46 at
eight – the round trip is ~20 ms of fixed cost, amortised by the batch.

### an.engines.capture.DEFAULT_BATCH_PIXELS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 4147200*

a supersample
counts k² times, an open shutter once per instant): at most this many per
round trip, twice this many waiting on the pool. A frame COUNT bounds nothing
when a frame is a k-times, many-sample, incompressible canvas (an#192 review:
one reply overflowed the driver’s string limit and the render hung).

* **Type:**
  The same two bounds in CAPTURED PIXELS (backbuffer pixels

### an.engines.capture.DEFAULT_ENCODE_WORKERS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 2*

Threads resolving and encoding frames while the engine draws the next batch.
The stage’s decode/encode is ~60 ms/frame of Pillow at 1080p, the same order
as the page’s own work, so it must overlap it. Two, not `cpu_count()`: a
parallel render already runs one engine per shot.

### an.engines.capture.DEFAULT_MAX_INFLIGHT *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 16*

frames handed to the pool and not yet written.

* **Type:**
  BACK-PRESSURE

### *exception* an.engines.capture.FrameStageError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The frame stage could not produce a frame directory it can vouch for.

A renderer built by `frame_stage_renderer(..., error=...)` re-raises it as
its own typed error at its boundary.

### an.engines.capture.capture_frames(session, requests, frames_dir, , factor=1, size=None, batch=None, workers=None, max_inflight=None, batch_pixels=None)

Write one PNG per request into `frames_dir`, resolved to the declared size.

`session` is time-driven (`frame(t)`), optionally batched
(`frames(requests)`), optionally with its own `resolve`. A state-driven
session is adapted by the frame stage before it reaches here. `size`, when
known, sets the pixel budget and is passed to the resolve. `None` for any
tunable reads the module default at call time.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.engines.capture.default_resolve(samples, , frame, factor, size)

The core’s resolve: spatially by `factor`, then the instants in time.

One sample at factor 1 is returned as is – the engine’s own bytes.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)


# _autosummary/an.engines.conformance.html.md

# an.engines.conformance

Conformance of a TIME-driven engine against the timing kernel’s golden vectors.

A time-driven engine evaluates the compiled channels itself (core study §2.7):
`runtime.js` does, in a browser. Its pictures are only comparable with any
other engine’s if the STATE it evaluated at `t` is the kernel’s, so a
time-driven session exposes `state(t)` – the read-back – and this module
holds it to `an/data/timing/timing_vectors.json`, the contract `an.timing`
and `previz` both assert (ADR 0001 decision 10): numbers within the file’s
tolerance, everything else exact, every sample of every case of the session’s
property space.

The engine supplies how to load a vector’s document; the check is the core’s.

```pycon
>>> cases = vector_cases(space="stage.node")
>>> len(cases) >= 10 and all(c["space"] == "stage.node" for c in cases)
True
>>> readback_mismatches(lambda t: {}, {"name": "n", "samples": [{"t": 0.0, "state": {}}]})
[]
```

### Module Attributes

| [`VECTORS_RESOURCE`](_autosummary/an.engines.conformance.html.md#an.engines.conformance.VECTORS_RESOURCE)   | `(package, path inside it)` for [`importlib.resources`](https://docs.python.org/3/library/importlib.resources.html#module-importlib.resources).   |
|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|

### Functions

| [`as_contract_state`](_autosummary/an.engines.conformance.html.md#an.engines.conformance.as_contract_state)(state)             | A state in the vectors' spelling: `"target:property"` keys.                                                                                                                                                                                                                                                        |
|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`case_document`](_autosummary/an.engines.conformance.html.md#an.engines.conformance.case_document)(case)                  | The case's document, carrying its property space the way a compiled document does (an#287): `meta.entity_spaces` names the space for every entity the case animates and `meta.spaces` defines it, so an engine that reads only the DOCUMENT (`runtime.js` has no registry) evaluates the case in the case's space. |
| [`conformance_report`](_autosummary/an.engines.conformance.html.md#an.engines.conformance.conformance_report)(open_case, cases) | Every mismatch over `cases`; `open_case(case)` is a context manager yielding a session loaded with the case's document.                                                                                                                                                                                            |
| [`readback_mismatches`](_autosummary/an.engines.conformance.html.md#an.engines.conformance.readback_mismatches)(state_at, case)  | `(case, t, expected, got)` for every sample whose read-back differs.                                                                                                                                                                                                                                               |
| [`vector_cases`](_autosummary/an.engines.conformance.html.md#an.engines.conformance.vector_cases)(\*[, space])            | The golden cases, optionally only those of one property space.                                                                                                                                                                                                                                                     |

### an.engines.conformance.VECTORS_RESOURCE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= ('an.data', 'timing/timing_vectors.json')*

`(package, path inside it)` for [`importlib.resources`](https://docs.python.org/3/library/importlib.resources.html#module-importlib.resources).

* **Type:**
  Where the vectors ship

### an.engines.conformance.as_contract_state(state)

A state in the vectors’ spelling: `"target:property"` keys.

Accepts the kernel’s pose keys (`(target, property)` tuples) and the
runtime’s (`"target::property"`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> as_contract_state({("a", "x"): 1.0, "b::y": 2})
{'a:x': 1.0, 'b:y': 2}
```

### an.engines.conformance.case_document(case)

The case’s document, carrying its property space the way a compiled
document does (an#287): `meta.entity_spaces` names the space for every
entity the case animates and `meta.spaces` defines it, so an engine that
reads only the DOCUMENT (`runtime.js` has no registry) evaluates the case
in the case’s space. A case in the default space is returned as it is.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> case = {"space": {"name": "inline", "fields": []}, "document": {
...     "timeline": {"duration": 1.0, "tracks": []}, "animations": {"m": {
...     "duration": 1.0, "channels": [{"target": "view/a", "property": "x",
...     "keyframes": [{"time": 0.0, "value": 0}]}]}}}}
>>> meta = case_document(case)["meta"]
>>> meta["entity_spaces"], meta["spaces"]["inline"]["fields"]
({'view': 'inline'}, [])
>>> "meta" in case_document({"space": "stage.node", "document": case["document"]})
False
```

### an.engines.conformance.conformance_report(open_case, cases)

Every mismatch over `cases`; `open_case(case)` is a context manager
yielding a session loaded with the case’s document.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.engines.conformance.readback_mismatches(state_at, case)

`(case, t, expected, got)` for every sample whose read-back differs.

`state_at` is a loaded session’s `state` (or anything with its shape).
Compared with the contract’s own rule ([`an.timing.contract.values_close()`](_autosummary/an.timing.contract.html.md#an.timing.contract.values_close)).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.engines.conformance.vector_cases(, space=None)

The golden cases, optionally only those of one property space.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]


# _autosummary/an.engines.frame_stage.html.md

# an.engines.frame_stage

`frame_stage_renderer(engine)`: any seekable engine becomes a `Renderer`.

The core owns everything around the engine, once, for every engine:

1. the knobs, validated before anything launches – ffmpeg present, the
   supersample factor ([`an.media.supersample.check_factor()`](_autosummary/an.media.supersample.html.md#an.media.supersample.check_factor)), the pixel
   format ([`an.media.mp4.check_pix_fmt()`](_autosummary/an.media.mp4.html.md#an.media.mp4.check_pix_fmt)), the engine’s own knobs
   (`Engine.check`), and the frame clock ([`an.media.shutter.check_frame_samples()`](_autosummary/an.media.shutter.html.md#an.media.shutter.check_frame_samples));
2. the clock: `frame_count = max(1, round(duration * fps))`, frame `i` at
   `i / fps` unless `RenderContext.frame_samples` (built by
   [`an.frame_clock.FrameClock`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock)) says otherwise;
3. the shot’s workspace – `<work_dir>/shot_<id>/` with a cleared `frames/`;
4. the capture loop ([`an.engines.capture`](_autosummary/an.engines.capture.html.md#module-an.engines.capture)): supersampling and the shutter
   resolved in the frame stage;
5. the sink: the frames muxed to the shot mp4 with the pinned argv, the shot’s
   dialogue laid under it ([`an.media.mp4.mux_shot()`](_autosummary/an.media.mp4.html.md#an.media.mp4.mux_shot));
6. provenance: the core’s facts plus the session’s own (`provenance()`).

The engine only loads the shot and draws instants ([`an.engines.protocol`](_autosummary/an.engines.protocol.html.md#module-an.engines.protocol)).
A STATE-driven session is adapted here: the core evaluates its `timeline` with
[`an.timing.timeline.evaluate_timeline()`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.evaluate_timeline) (in the session’s `space` when it
has one) and hands each state to `render`.

Errors the core raises ([`FrameStageError`](_autosummary/an.engines.capture.html.md#an.engines.capture.FrameStageError),
[`MediaError`](_autosummary/an.media.mp4.html.md#an.media.mp4.MediaError), [`UnseekableEngineError`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.UnseekableEngineError))
are re-raised as the renderer’s own `error` type at its boundary, message
intact, so a cut-out render still fails with `CutoutRenderError`.

```pycon
>>> from contextlib import contextmanager
>>> class Card:
...     name = "card"
...     @contextmanager
...     def open(self, job):
...         yield self
...     def frame(self, t): return b""
...     def state(self, t): return {}
>>> r = frame_stage_renderer(Card(), renderers=("card", "title"))
>>> r.name, r.supported_renderers
('card', ('card', 'title'))
```

### Module Attributes

| [`SHOT_WORKSPACE_PATTERN`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.SHOT_WORKSPACE_PATTERN)   | A shot's scratch directory under `RenderContext.work_dir`.   |
|---------------------------------------------------------------------------|--------------------------------------------------------------|

### Functions

| [`frame_stage_renderer`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.frame_stage_renderer)(engine, \*[, name, ...])   | Turn `engine` into a `Renderer`: the core's clock, capture loop, resolves and sinks.   |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|
| [`require_engine`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.require_engine)(engine)                          | Refuse an object that is not an engine, saying what is missing.                        |
| [`shot_workspace`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.shot_workspace)(work_dir, shot_id)               | `<work_dir>/shot_<id>`: the one per-shot scratch directory.                            |

### Classes

| [`FrameStageRenderer`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.FrameStageRenderer)(engine[, name, ...])   | A `Renderer` that drives an [`Engine`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine) frame by frame.   |
|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| [`StateDrivenAdapter`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.StateDrivenAdapter)(session)               | A state-driven session seen as a time-driven one: the core evaluates `at(t)`.                                                    |

### *class* an.engines.frame_stage.FrameStageRenderer(engine, name='', supported_renderers=(), error=<class 'an.engines.capture.FrameStageError'>, capture_options=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A `Renderer` that drives an [`Engine`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine) frame by frame.

Build it with [`frame_stage_renderer()`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.frame_stage_renderer). Stateless across renders, so one
instance serves every shot of a parallel render: each `render` opens its
own session.

#### capture_options *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

Capture-loop tunables passed through to [`an.engines.capture.capture_frames()`](_autosummary/an.engines.capture.html.md#an.engines.capture.capture_frames)
(`batch`, `workers`, `max_inflight`, `batch_pixels`); unset = its defaults.

#### error

The exception type raised at the boundary for every core error.

alias of [`FrameStageError`](_autosummary/an.engines.capture.html.md#an.engines.capture.FrameStageError)

#### probe_frames(shot, ctx, times)

The film’s frames of `shot` showing at each of `times` (seconds), as PNG bytes.

`an probe` (an#347): the session [`render()`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.FrameStageRenderer.render) opens (the same
engine, state-driven adapter, supersample and frame clock) and the
same [`capture_frames()`](_autosummary/an.engines.capture.html.md#an.engines.capture.capture_frames), asked only for the
frames needed — so a probe frame IS the film’s frame, without the
rest of the shot or the mux. Each instant is snapped to the frame
showing then (`floor(t * fps)`, clamped to the shot).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

#### render(shot, ctx)

Render `shot` to mp4 through the engine; see the module docstring.

* **Return type:**
  [`RenderResult`](_autosummary/an.adapters.html.md#an.adapters.RenderResult)

#### supported_renderers *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ()*

The `Shot.renderer` values this renderer claims (the ONE place it names them).

### an.engines.frame_stage.SHOT_WORKSPACE_PATTERN *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'shot_{shot_id}'*

A shot’s scratch directory under `RenderContext.work_dir`. Kept from the
stage renderer it came from, so the paths a render leaves behind (and that
the bench and film assembly read) do not move.

### *class* an.engines.frame_stage.StateDrivenAdapter(session)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A state-driven session seen as a time-driven one: the core evaluates `at(t)`.

`frame(t)` is `render(evaluate_timeline(timeline, t, space=space))` and
`state(t)` is the evaluated state, so the capture loop and the conformance
tests treat both drive modes alike. The session’s other members
(`resolve`, `provenance`, …) are reached through attribute access.

**Batched** (an#286): `frames(requests)` evaluates every instant here and
hands the session the STATES, through its optional `render_states(states)`
(one round trip per batch, the member a JS-bridged view engine implements),
else `render` per state. A session’s own `frames` is never forwarded: it
would take times and bypass `state`.

**Adapted by drive mode** (an#286): `project(point, t)` and `bounds(t)`
reach the session as `project(point, state)` and `bounds(state)` (the
state-driven signature, previz’s `project(point, {state, ...})`), and
exist only when the session has them, so [`describe()`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.describe)
still reads capabilities from real members.

#### frames(requests)

One list of frames per request, a frame per instant, in order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]

### an.engines.frame_stage.frame_stage_renderer(engine, \*, name=None, renderers=None, error=<class 'an.engines.capture.FrameStageError'>, capture_options=None)

Turn `engine` into a `Renderer`: the core’s clock, capture loop, resolves and sinks.

`name` defaults to the engine’s; `renderers` – the `Shot.renderer`
values claimed – default to `(name,)`. `error` is the typed exception
the renderer raises at its boundary for every core error.

* **Return type:**
  [`FrameStageRenderer`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.FrameStageRenderer)

### an.engines.frame_stage.require_engine(engine)

Refuse an object that is not an engine, saying what is missing.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> require_engine(object())
Traceback (most recent call last):
  ...
TypeError: an Engine needs `name` and `open(job)`; object lacks: name, open
```

### an.engines.frame_stage.shot_workspace(work_dir, shot_id)

`<work_dir>/shot_<id>`: the one per-shot scratch directory.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.engines.html.md

# an.engines

Engines: seekable things the core drives frame by frame, and the renderer that drives them.

Core (ADR 0001 decision 12; core study §2.7). No engine is core: the core holds
the PROTOCOL and the frame stage, and an engine – the 2D stage runtime, a
`previz` view behind a page, a `burns` crop over a still – lives in its own
package and registers the renderer [`frame_stage_renderer()`](_autosummary/an.engines.html.md#an.engines.frame_stage_renderer) builds from it.

- [`an.engines.protocol`](_autosummary/an.engines.protocol.html.md#module-an.engines.protocol) – `Engine` (a factory that opens a session per
  shot), the time-driven and state-driven session protocols, the declared live
  tier, and [`describe()`](_autosummary/an.engines.html.md#an.engines.describe), which reads a session’s tier, drive mode and
  features off the members it implements (never off a flag).
- [`an.engines.capture`](_autosummary/an.engines.capture.html.md#module-an.engines.capture) – the capture loop, sequential or batched, with the
  frame stage’s resolves (supersample, shutter).
- [`an.engines.frame_stage`](_autosummary/an.engines.frame_stage.html.md#module-an.engines.frame_stage) – [`frame_stage_renderer()`](_autosummary/an.engines.html.md#an.engines.frame_stage_renderer): an engine in, a
  `Renderer` out, using the core’s frame clock, capture loop, resolves and MP4
  sink ([`an.media`](_autosummary/an.media.html.md#module-an.media)).

```pycon
>>> from contextlib import contextmanager
>>> class Blank:
...     name = "blank"
...     @contextmanager
...     def open(self, job):
...         yield self
...     def frame(self, t): return b""
...     def state(self, t): return {}
>>> describe(Blank()).drive, frame_stage_renderer(Blank()).name
('time', 'blank')
```

### Functions

| [`capture_frames`](_autosummary/an.engines.html.md#an.engines.capture_frames)(session, requests, frames_dir, \*)   | Write one PNG per request into `frames_dir`, resolved to the declared size.          |
|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`describe`](_autosummary/an.engines.html.md#an.engines.describe)(session)                                   | The tier, drive mode and features a session (or a session class) offers.             |
| [`drive_mode`](_autosummary/an.engines.html.md#an.engines.drive_mode)(session)                                 | `"time"`, `"state"`, or `None` for a session the frame stage cannot drive.           |
| [`frame_stage_renderer`](_autosummary/an.engines.html.md#an.engines.frame_stage_renderer)(engine, \*[, name, ...])       | Turn `engine` into a `Renderer`: the core's clock, capture loop, resolves and sinks. |
| [`require_engine`](_autosummary/an.engines.html.md#an.engines.require_engine)(engine)                              | Refuse an object that is not an engine, saying what is missing.                      |
| [`require_seekable`](_autosummary/an.engines.html.md#an.engines.require_seekable)(session)                           | The session's drive mode, or a refusal that says what to add.                        |
| [`requests_as_dicts`](_autosummary/an.engines.html.md#an.engines.requests_as_dicts)(requests)                         | Requests as plain dicts, the shape a page or a subprocess takes.                     |
| [`shot_workspace`](_autosummary/an.engines.html.md#an.engines.shot_workspace)(work_dir, shot_id)                   | `<work_dir>/shot_<id>`: the one per-shot scratch directory.                          |

### Classes

| [`Engine`](_autosummary/an.engines.html.md#an.engines.Engine)(\*args, \*\*kwargs)                      | A stateless factory of loaded, seekable sessions -- one per shot render.                                                       |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| [`EngineProfile`](_autosummary/an.engines.html.md#an.engines.EngineProfile)(tier, drive, features)            | What a session can do, read off its members.                                                                                   |
| [`FrameJob`](_autosummary/an.engines.html.md#an.engines.FrameJob)(shot, ctx, workspace, frames_dir, ...) | What the frame stage asks of an engine for one shot.                                                                           |
| [`FrameRequest`](_autosummary/an.engines.html.md#an.engines.FrameRequest)(frame, times)                      | One output frame's instants, in the order they must be captured.                                                               |
| [`FrameStageRenderer`](_autosummary/an.engines.html.md#an.engines.FrameStageRenderer)(engine[, name, ...])         | A `Renderer` that drives an [`Engine`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine) frame by frame. |
| [`LiveEngine`](_autosummary/an.engines.html.md#an.engines.LiveEngine)(\*args, \*\*kwargs)                  | The live tier: apply, settle, capture in real time.                                                                            |
| [`StateDriven`](_autosummary/an.engines.html.md#an.engines.StateDriven)(\*args, \*\*kwargs)                 | A session the core hands states to.                                                                                            |
| [`StateDrivenAdapter`](_autosummary/an.engines.html.md#an.engines.StateDrivenAdapter)(session)                     | A state-driven session seen as a time-driven one: the core evaluates `at(t)`.                                                  |
| [`TimeDriven`](_autosummary/an.engines.html.md#an.engines.TimeDriven)(\*args, \*\*kwargs)                  | A session that evaluates the compiled channels itself.                                                                         |

### Exceptions

| [`FrameStageError`](_autosummary/an.engines.html.md#an.engines.FrameStageError)       | The frame stage could not produce a frame directory it can vouch for.      |
|------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`UnseekableEngineError`](_autosummary/an.engines.html.md#an.engines.UnseekableEngineError) | A session that the frame stage cannot drive; the message says what to add. |

### *class* an.engines.Engine(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

A stateless factory of loaded, seekable sessions – one per shot render.

#### name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The engine’s name (`"stage"`); also how provenance names it.

#### open(job)

Load `job.shot` and yield a session (see the module table).

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* an.engines.EngineProfile(tier, drive, features)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a session can do, read off its members.

### *class* an.engines.FrameJob(shot, ctx, workspace, frames_dir, total_frames, supersample, frame_samples=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What the frame stage asks of an engine for one shot.

Built by the core, before [`Engine.open()`](_autosummary/an.engines.html.md#an.engines.Engine.open): every knob here is already
validated, so an engine never re-derives the frame count or the factor.

#### frame_samples *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), ...], ...] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per frame, the instants to capture and average, or `None` for one
instant at `i / fps`.

#### requests()

Every frame’s request, frame `i` at `i / fps` unless a clock says otherwise.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`FrameRequest`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.FrameRequest), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> from types import SimpleNamespace
>>> job = FrameJob(None, SimpleNamespace(fps=4, resolution=(2, 2)), Path("."),
...                Path("."), total_frames=3, supersample=1)
>>> [r.times for r in job.requests()]
[(0.0,), (0.25,), (0.5,)]
```

#### *property* size *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int)]*

what every resolved frame must be.

* **Type:**
  The DECLARED ([*width*](_autosummary/an.stage.paths.html.md#an.stage.paths.PathDescriptor.width), height)

#### supersample *: [int](https://docs.python.org/3/builtins/functions.html#int)*

the engine draws at `k` times the
declared size, and the core resolves back to it.

* **Type:**
  The validated supersample factor

#### workspace *: [Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path)*

The shot’s own scratch directory (`<work_dir>/shot_<id>`). The engine
may stage files in it (the stage engine’s runtime copy); the core owns
`frames/` and the delivered mp4 inside it.

### *class* an.engines.FrameRequest(frame, times)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One output frame’s instants, in the order they must be captured.

One instant unless a frame clock opened the shutter
(`RenderContext.frame_samples`).

### *exception* an.engines.FrameStageError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The frame stage could not produce a frame directory it can vouch for.

A renderer built by `frame_stage_renderer(..., error=...)` re-raises it as
its own typed error at its boundary.

### *class* an.engines.FrameStageRenderer(engine, name='', supported_renderers=(), error=<class 'an.engines.capture.FrameStageError'>, capture_options=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A `Renderer` that drives an [`Engine`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine) frame by frame.

Build it with [`frame_stage_renderer()`](_autosummary/an.engines.html.md#an.engines.frame_stage_renderer). Stateless across renders, so one
instance serves every shot of a parallel render: each `render` opens its
own session.

#### capture_options *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

Capture-loop tunables passed through to [`an.engines.capture.capture_frames()`](_autosummary/an.engines.capture.html.md#an.engines.capture.capture_frames)
(`batch`, `workers`, `max_inflight`, `batch_pixels`); unset = its defaults.

#### error

alias of [`FrameStageError`](_autosummary/an.engines.capture.html.md#an.engines.capture.FrameStageError)

#### probe_frames(shot, ctx, times)

The film’s frames of `shot` showing at each of `times` (seconds), as PNG bytes.

`an probe` (an#347): the session [`render()`](_autosummary/an.engines.html.md#an.engines.FrameStageRenderer.render) opens (the same
engine, state-driven adapter, supersample and frame clock) and the
same [`capture_frames()`](_autosummary/an.engines.capture.html.md#an.engines.capture.capture_frames), asked only for the
frames needed — so a probe frame IS the film’s frame, without the
rest of the shot or the mux. Each instant is snapped to the frame
showing then (`floor(t * fps)`, clamped to the shot).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

#### render(shot, ctx)

Render `shot` to mp4 through the engine; see the module docstring.

* **Return type:**
  [`RenderResult`](_autosummary/an.adapters.html.md#an.adapters.RenderResult)

#### supported_renderers *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ()*

The `Shot.renderer` values this renderer claims (the ONE place it names them).

### *class* an.engines.LiveEngine(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

The live tier: apply, settle, capture in real time. **Declared, not built.**

Its recorder resamples to a constant rate (`walkthru`’s worked example);
`frame_stage_renderer` does not drive it, and never falls back to it.

### *class* an.engines.StateDriven(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

A session the core hands states to.

#### render(state)

PNG bytes of `state` (`supersample` times the declared size).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

#### timeline *: [Any](https://docs.python.org/3/library/typing.html#typing.Any)*

The `an.timing` `Timeline` the core evaluates.

### *class* an.engines.StateDrivenAdapter(session)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A state-driven session seen as a time-driven one: the core evaluates `at(t)`.

`frame(t)` is `render(evaluate_timeline(timeline, t, space=space))` and
`state(t)` is the evaluated state, so the capture loop and the conformance
tests treat both drive modes alike. The session’s other members
(`resolve`, `provenance`, …) are reached through attribute access.

**Batched** (an#286): `frames(requests)` evaluates every instant here and
hands the session the STATES, through its optional `render_states(states)`
(one round trip per batch, the member a JS-bridged view engine implements),
else `render` per state. A session’s own `frames` is never forwarded: it
would take times and bypass `state`.

**Adapted by drive mode** (an#286): `project(point, t)` and `bounds(t)`
reach the session as `project(point, state)` and `bounds(state)` (the
state-driven signature, previz’s `project(point, {state, ...})`), and
exist only when the session has them, so [`describe()`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.describe)
still reads capabilities from real members.

#### frames(requests)

One list of frames per request, a frame per instant, in order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]

### *class* an.engines.TimeDriven(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

A session that evaluates the compiled channels itself.

#### frame(t)

The scene at `t` as PNG bytes (`supersample` times the declared size).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

#### state(t)

The state the engine evaluated at `t`: a sparse pose, absent = at rest.

* **Return type:**
  [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *exception* an.engines.UnseekableEngineError

Bases: [`TypeError`](https://docs.python.org/3/builtins/exceptions.html#TypeError)

A session that the frame stage cannot drive; the message says what to add.

### an.engines.capture_frames(session, requests, frames_dir, , factor=1, size=None, batch=None, workers=None, max_inflight=None, batch_pixels=None)

Write one PNG per request into `frames_dir`, resolved to the declared size.

`session` is time-driven (`frame(t)`), optionally batched
(`frames(requests)`), optionally with its own `resolve`. A state-driven
session is adapted by the frame stage before it reaches here. `size`, when
known, sets the pixel budget and is passed to the resolve. `None` for any
tunable reads the module default at call time.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.engines.describe(session)

The tier, drive mode and features a session (or a session class) offers.

* **Return type:**
  [`EngineProfile`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.EngineProfile)

### an.engines.drive_mode(session)

`"time"`, `"state"`, or `None` for a session the frame stage cannot drive.

A session with both `frame` and `render` is time-driven: it evaluates
its own document, and the core does not second-guess it.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.engines.frame_stage_renderer(engine, \*, name=None, renderers=None, error=<class 'an.engines.capture.FrameStageError'>, capture_options=None)

Turn `engine` into a `Renderer`: the core’s clock, capture loop, resolves and sinks.

`name` defaults to the engine’s; `renderers` – the `Shot.renderer`
values claimed – default to `(name,)`. `error` is the typed exception
the renderer raises at its boundary for every core error.

* **Return type:**
  [`FrameStageRenderer`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.FrameStageRenderer)

### an.engines.requests_as_dicts(requests)

Requests as plain dicts, the shape a page or a subprocess takes.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.engines.require_engine(engine)

Refuse an object that is not an engine, saying what is missing.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> require_engine(object())
Traceback (most recent call last):
  ...
TypeError: an Engine needs `name` and `open(job)`; object lacks: name, open
```

### an.engines.require_seekable(session)

The session’s drive mode, or a refusal that says what to add.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> require_seekable(object())
Traceback (most recent call last):
  ...
an.engines.protocol.UnseekableEngineError: this engine session cannot be captured: it has no frame(t) (time-driven), and no render(state) with a timeline (state-driven). Add frame(t) and state(t) if the engine evaluates the timeline itself, or timeline and render(state) if the core should evaluate it.
```

### an.engines.shot_workspace(work_dir, shot_id)

`<work_dir>/shot_<id>`: the one per-shot scratch directory.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### Modules

| [`capture`](_autosummary/an.engines.capture.html.md#module-an.engines.capture)         | The capture loop: drive a session through every frame, resolve, write -- for ANY engine.   |
|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| [`conformance`](_autosummary/an.engines.conformance.html.md#module-an.engines.conformance) | Conformance of a TIME-driven engine against the timing kernel's golden vectors.            |
| [`frame_stage`](_autosummary/an.engines.frame_stage.html.md#module-an.engines.frame_stage) | `frame_stage_renderer(engine)`: any seekable engine becomes a `Renderer`.                  |
| [`protocol`](_autosummary/an.engines.protocol.html.md#module-an.engines.protocol)       | The `Engine` protocol: a seekable thing that turns a time, or a state, into a frame.       |


# _autosummary/an.engines.protocol.html.md

# an.engines.protocol

The `Engine` protocol: a seekable thing that turns a time, or a state, into a frame.

The second of the three renderer tiers (ADR 0001 decision 12; core study §2.7):

1. `Renderer` (`an.adapters._base`) – shot in, media file out. Every back-end
   is reachable through it; Manim lives there, whole-shot.
2. **\`\`Engine\`\`** (this module) – `previz`’s meaning of the word: something
   that can show the scene at any instant and hand over the pixels. The core
   turns any engine into a `Renderer` with [`an.engines.frame_stage_renderer()`](_autosummary/an.engines.html.md#an.engines.frame_stage_renderer),
   which owns the clock, the capture loop, supersampling, the shutter and the
   sinks, so an engine implements none of them.
3. `LiveEngine` – apply, settle, capture in real time (`walkthru`’s recorder).
   **Declared, not built.**

**Capabilities are read from which members exist, never from a flag**
(`previz`’s rule, ADR 0002 applied to engines), so a flag can never disagree
with the code. [`describe()`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.describe) is the one place that reads them.

An `Engine` is a stateless factory, safe to share across the threads of a
parallel render. [`Engine.open()`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine.open) loads one shot and yields a **session**:
the loaded, seekable view, whose members say what it can do.

And on the `Engine` itself: `check(ctx)` validates the engine’s own knobs
before anything launches (a typo must fail in microseconds, not after a browser
started).

```pycon
>>> class _Still:
...     def frame(self, t): return b"png"
...     def state(self, t): return {}
>>> describe(_Still())
EngineProfile(tier='seekable', drive='time', features=frozenset({'readback'}))
>>> class _Crop:
...     timeline = None
...     def render(self, state): return b"png"
>>> describe(_Crop()).drive
'state'
```

### Module Attributes

| [`DRIVE_TIME`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.DRIVE_TIME)      | who evaluates the timeline.                                       |
|------------------------------------------------------------------|-------------------------------------------------------------------|
| [`TIER_SEEKABLE`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.TIER_SEEKABLE)   | Tiers an engine can be on (the `Renderer` tier is not an engine). |
| [`FEATURE_MEMBERS`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.FEATURE_MEMBERS) | Optional session member -> the feature it unlocks.                |

### Functions

| [`describe`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.describe)(session)           | The tier, drive mode and features a session (or a session class) offers.   |
|------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`drive_mode`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.drive_mode)(session)         | `"time"`, `"state"`, or `None` for a session the frame stage cannot drive. |
| [`require_seekable`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.require_seekable)(session)   | The session's drive mode, or a refusal that says what to add.              |
| [`requests_as_dicts`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.requests_as_dicts)(requests) | Requests as plain dicts, the shape a page or a subprocess takes.           |

### Classes

| [`Engine`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine)(\*args, \*\*kwargs)                      | A stateless factory of loaded, seekable sessions -- one per shot render.   |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`EngineProfile`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.EngineProfile)(tier, drive, features)            | What a session can do, read off its members.                               |
| [`FrameJob`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.FrameJob)(shot, ctx, workspace, frames_dir, ...) | What the frame stage asks of an engine for one shot.                       |
| [`FrameRequest`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.FrameRequest)(frame, times)                      | One output frame's instants, in the order they must be captured.           |
| [`LiveEngine`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.LiveEngine)(\*args, \*\*kwargs)                  | The live tier: apply, settle, capture in real time.                        |
| [`StateDriven`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.StateDriven)(\*args, \*\*kwargs)                 | A session the core hands states to.                                        |
| [`TimeDriven`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.TimeDriven)(\*args, \*\*kwargs)                  | A session that evaluates the compiled channels itself.                     |

### Exceptions

| [`UnseekableEngineError`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.UnseekableEngineError)   | A session that the frame stage cannot drive; the message says what to add.   |
|--------------------------------------------------------------------------|------------------------------------------------------------------------------|

### an.engines.protocol.DRIVE_TIME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'time'*

who evaluates the timeline.

* **Type:**
  Drive modes (core study §2.7)

### *class* an.engines.protocol.Engine(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

A stateless factory of loaded, seekable sessions – one per shot render.

#### name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The engine’s name (`"stage"`); also how provenance names it.

#### open(job)

Load `job.shot` and yield a session (see the module table).

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* an.engines.protocol.EngineProfile(tier, drive, features)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a session can do, read off its members.

### an.engines.protocol.FEATURE_MEMBERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'bounds': 'bounds', 'frame_with_alpha': 'alpha', 'frames': 'batch', 'project': 'project', 'provenance': 'provenance', 'render_states': 'batch', 'resolve': 'resolve', 'state': 'readback'}*

Optional session member -> the feature it unlocks. The SSOT [`describe()`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.describe)
reads; a new feature is a new row, never a flag on an engine.

### *class* an.engines.protocol.FrameJob(shot, ctx, workspace, frames_dir, total_frames, supersample, frame_samples=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What the frame stage asks of an engine for one shot.

Built by the core, before [`Engine.open()`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine.open): every knob here is already
validated, so an engine never re-derives the frame count or the factor.

#### frame_samples *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), ...], ...] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per frame, the instants to capture and average, or `None` for one
instant at `i / fps`.

#### requests()

Every frame’s request, frame `i` at `i / fps` unless a clock says otherwise.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`FrameRequest`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.FrameRequest), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> from types import SimpleNamespace
>>> job = FrameJob(None, SimpleNamespace(fps=4, resolution=(2, 2)), Path("."),
...                Path("."), total_frames=3, supersample=1)
>>> [r.times for r in job.requests()]
[(0.0,), (0.25,), (0.5,)]
```

#### *property* size *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int)]*

what every resolved frame must be.

* **Type:**
  The DECLARED ([*width*](_autosummary/an.stage.paths.html.md#an.stage.paths.PathDescriptor.width), height)

#### supersample *: [int](https://docs.python.org/3/builtins/functions.html#int)*

the engine draws at `k` times the
declared size, and the core resolves back to it.

* **Type:**
  The validated supersample factor

#### workspace *: [Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path)*

The shot’s own scratch directory (`<work_dir>/shot_<id>`). The engine
may stage files in it (the stage engine’s runtime copy); the core owns
`frames/` and the delivered mp4 inside it.

### *class* an.engines.protocol.FrameRequest(frame, times)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One output frame’s instants, in the order they must be captured.

One instant unless a frame clock opened the shutter
(`RenderContext.frame_samples`).

### *class* an.engines.protocol.LiveEngine(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

The live tier: apply, settle, capture in real time. **Declared, not built.**

Its recorder resamples to a constant rate (`walkthru`’s worked example);
`frame_stage_renderer` does not drive it, and never falls back to it.

### *class* an.engines.protocol.StateDriven(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

A session the core hands states to.

#### render(state)

PNG bytes of `state` (`supersample` times the declared size).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

#### timeline *: [Any](https://docs.python.org/3/library/typing.html#typing.Any)*

The `an.timing` `Timeline` the core evaluates.

### an.engines.protocol.TIER_SEEKABLE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'seekable'*

Tiers an engine can be on (the `Renderer` tier is not an engine).

### *class* an.engines.protocol.TimeDriven(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

A session that evaluates the compiled channels itself.

#### frame(t)

The scene at `t` as PNG bytes (`supersample` times the declared size).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

#### state(t)

The state the engine evaluated at `t`: a sparse pose, absent = at rest.

* **Return type:**
  [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *exception* an.engines.protocol.UnseekableEngineError

Bases: [`TypeError`](https://docs.python.org/3/builtins/exceptions.html#TypeError)

A session that the frame stage cannot drive; the message says what to add.

### an.engines.protocol.describe(session)

The tier, drive mode and features a session (or a session class) offers.

* **Return type:**
  [`EngineProfile`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.EngineProfile)

### an.engines.protocol.drive_mode(session)

`"time"`, `"state"`, or `None` for a session the frame stage cannot drive.

A session with both `frame` and `render` is time-driven: it evaluates
its own document, and the core does not second-guess it.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.engines.protocol.requests_as_dicts(requests)

Requests as plain dicts, the shape a page or a subprocess takes.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.engines.protocol.require_seekable(session)

The session’s drive mode, or a refusal that says what to add.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> require_seekable(object())
Traceback (most recent call last):
  ...
an.engines.protocol.UnseekableEngineError: this engine session cannot be captured: it has no frame(t) (time-driven), and no render(state) with a timeline (state-driven). Add frame(t) and state(t) if the engine evaluates the timeline itself, or timeline and render(state) if the core should evaluate it.
```


# _autosummary/an.environments.html.md

# an.environments

Moved to [`an.stage.environments`](_autosummary/an.stage.environments.html.md#module-an.stage.environments) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.formats.html.md

# an.formats

Numbers as text: a declared subset of d3-format, for counters (an#342).

A counter text block ([`an.stage.text.TextDescriptor`](_autosummary/an.stage.text.html.md#an.stage.text.TextDescriptor) `counter`) shows a
number that moves; what it draws is a string, and this module is the one
statement of how the number becomes the string. The format is a \*\*declared
mini-language, not Python’s\*\* `str.format` (which allows attribute access,
states no rounding and is not reproducible in a browser): one d3-format
specifier in braces, with optional literal text around it.

```pycon
>>> format_number(7, "Day {d}")
'Day 7'
>>> format_number(1234567, "{,d}")
'1,234,567'
>>> format_number(21.456, "{.1f} °C")
'21.5 °C'
>>> format_number(0.256, "{.0%}")
'26%'
>>> format_number(1500, "{.2s}")
'1.5k'
```

The subset, and what each specifier means (d3-format’s meaning, so a JavaScript
consumer can reproduce it with `d3.format`):

| `d`   | an integer                                                                                                                                                             |
|-------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `,d`  | an integer with `,` between thousands                                                                                                                                  |
| `.Nf` | fixed point, `N` decimals                                                                                                                                              |
| `.N%` | times 100, fixed point, `N` decimals, then `%`                                                                                                                         |
| `s`   | `N` significant digits (`.Ns`; default 6) with an SI prefix<br/>(`k`, `M`, `G`, …, `m`; `µ` and smaller are refused at<br/>typesetting by a face that lacks the glyph) |

**Rounding is to nearest, ties to even, on the float’s exact value** (Python’s
own correctly rounded formatting): `2.5` → `2` and `3.5` → `4` under
`d`; `0.125` → `0.12` under `.2f`. d3 rounds ties away from zero, so a
JavaScript reproduction must round half to even to agree on exact ties (the
frame grid rarely lands on one). Two further departures from d3’s defaults,
both stated: the minus sign is `-` (U+002D), because the embedded face has no
U+2212, and a value that rounds to zero never carries a sign (`-0.4` → `0`),
which is d3’s own rule.

### Module Attributes

| [`DFLT_SI_PRECISION`](_autosummary/an.formats.html.md#an.formats.DFLT_SI_PRECISION)   | d3's default of 6 significant digits.   |
|----------------------------------------------------------------------|-----------------------------------------|

### Functions

| [`parse_format`](_autosummary/an.formats.html.md#an.formats.parse_format)(fmt)         | Parse `fmt`: literal text around exactly one `{specifier}`.   |
|----------------------------------------------------------------------------|---------------------------------------------------------------|
| [`format_number`](_autosummary/an.formats.html.md#an.formats.format_number)(value, fmt) | `value` drawn as `fmt` says (see the module docstring).       |

### Classes

| [`NumberFormat`](_autosummary/an.formats.html.md#an.formats.NumberFormat)(prefix, type, precision, comma, ...)   | A parsed format: literal `prefix`, the specifier, literal `suffix`.   |
|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|

### Exceptions

| [`CounterFormatError`](_autosummary/an.formats.html.md#an.formats.CounterFormatError)   | A counter `format` outside the declared subset.   |
|-----------------------------------------------------------------------|---------------------------------------------------|

### *exception* an.formats.CounterFormatError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A counter `format` outside the declared subset.

### an.formats.DFLT_SI_PRECISION *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 6*

d3’s default of 6 significant digits.

* **Type:**
  `s` with no precision

### *class* an.formats.NumberFormat(prefix, type, precision, comma, suffix)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A parsed format: literal `prefix`, the specifier, literal `suffix`.

### an.formats.format_number(value, fmt)

`value` drawn as `fmt` says (see the module docstring).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> [format_number(v, "{d}") for v in (2.5, 3.5, -0.4, 29.5)]
['2', '4', '0', '30']
>>> format_number(-1234.5, "{,d}")
'-1,234'
```

### an.formats.parse_format(fmt)

Parse `fmt`: literal text around exactly one `{specifier}`.

* **Return type:**
  [`NumberFormat`](_autosummary/an.formats.html.md#an.formats.NumberFormat)

```pycon
>>> parse_format("Day {d}").prefix
'Day '
>>> parse_format("{.3d}")
Traceback (most recent call last):
...
an.formats.CounterFormatError: counter format '{.3d}': `d` takes no precision (it is an integer); the subset is d, ,d, .Nf, .N%, s, .Ns
```


# _autosummary/an.frame_clock.html.md

# an.frame_clock

The frame clock: WHEN each output frame samples scene time.

A render has always sampled frame `i` at exactly `i / fps` — one instant,
on a perfect grid. A real camera does neither: its shutter stays open for part
of the frame period (so a moving object smears), and its capture instants
wander around the nominal grid (so the timestamp a frame carries is not quite
when it was taken). Anything that wants to *measure* motion from video — the
sub-frame impact estimators `an.impacts` exists to score — needs both, and
needs to know exactly what was done.

[`FrameClock`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock) is that model as data. [`FrameClock.frames()`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock.frames) returns one
[`CapturedFrame`](_autosummary/an.frame_clock.html.md#an.frame_clock.CapturedFrame) per output frame, carrying the exposure interval and the
instants integrated over it; [`FrameClock.sample_times()`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock.sample_times) is the projection
the renderer consumes through `RenderContext.frame_samples`. One object feeds
both the render and the ground truth, so the two cannot disagree about when a
frame was taken.

The default clock is the old behaviour exactly — no exposure, no jitter, one
sample at `i / fps`:

```pycon
>>> FrameClock(fps=4).sample_times(1.0)
((0.0,), (0.25,), (0.5,), (0.75,))
```

A 180-degree shutter (`exposure=0.5`) integrates the first half of each frame
period, sampled at the midpoints of `samples` equal sub-intervals:

```pycon
>>> FrameClock(fps=4, exposure=0.5, samples=2).sample_times(0.5)
((0.03125, 0.09375), (0.28125, 0.34375))
>>> f = FrameClock(fps=4, exposure=0.5, samples=2).frames(0.5)[1]
>>> (f.t_nominal, f.t_open, f.t_close, f.t_mid)
(0.25, 0.25, 0.375, 0.3125)
```

Capture jitter moves each frame’s exposure as a unit, never out of order:

```pycon
>>> clock = FrameClock(fps=30, jitter_sd=0.004, seed=7)
>>> opens = [f.t_open for f in clock.frames(2.0)]
>>> all(b > a for a, b in zip(opens, opens[1:]))
True
>>> clock.frames(2.0) == clock.frames(2.0)  # seeded: the same clock twice
True
```

### Module Attributes

| [`DEFAULT_EXPOSURE_SAMPLES`](_autosummary/an.frame_clock.html.md#an.frame_clock.DEFAULT_EXPOSURE_SAMPLES)   | Sub-samples integrated per frame when the shutter is open and the caller did not say how many.                                                      |
|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| [`MAX_JITTER_FRACTION`](_autosummary/an.frame_clock.html.md#an.frame_clock.MAX_JITTER_FRACTION)        | A frame's capture offset is clamped to this fraction of the slack between one exposure closing and the next opening, on each side.                  |
| [`MAX_REPORT_NOISE_FRACTION`](_autosummary/an.frame_clock.html.md#an.frame_clock.MAX_REPORT_NOISE_FRACTION)  | Report noise is clamped to this fraction of the frame period, each side, so two neighbouring reported timestamps can never cross on a regular grid. |

### Functions

| [`frame_count`](_autosummary/an.frame_clock.html.md#an.frame_clock.frame_count)(duration, fps)   | Frames in a render of `duration` seconds — the renderer's own rule.   |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------|

### Classes

| [`CapturedFrame`](_autosummary/an.frame_clock.html.md#an.frame_clock.CapturedFrame)(index, t_nominal, t_open, ...)   | One output frame: when its exposure opened and closed, and what it saw.   |
|-------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`FrameClock`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock)([fps, exposure, samples, ...])      | A camera's timing, as data.                                               |

### Exceptions

| [`FrameClockError`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClockError)   | A frame clock that cannot describe a camera.   |
|--------------------------------------------------------------------|------------------------------------------------|

### *class* an.frame_clock.CapturedFrame(index, t_nominal, t_open, t_close, samples, t_reported)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One output frame: when its exposure opened and closed, and what it saw.

`t_nominal` is `index / fps` — what a constant-rate container (an mp4)
says the frame’s time is. `samples` are the instants actually rendered
and averaged into it; `t_mid` is the middle of the exposure, the single
best instant to attribute a blurred frame to. `t_reported` is the
timestamp a capture pipeline hands downstream: nominal or actual, per the
clock’s `timestamps` setting.

### an.frame_clock.DEFAULT_EXPOSURE_SAMPLES *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 8*

Sub-samples integrated per frame when the shutter is open and the caller did
not say how many. Eight midpoint samples put the smear of an object crossing
100 px in one exposure at steps of ~12 px — continuous to a tracker, cheap to
render (each sample is one screenshot).

### *class* an.frame_clock.FrameClock(fps=30.0, exposure=0.0, samples=None, jitter_sd=0.0, phase=0.0, timestamps='nominal', report_noise_sd=0.0, seed=0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A camera’s timing, as data. Every field defaults to the ideal camera.

`fps`: nominal frame rate (need not be an integer: 29.97 is a camera).
`exposure`: fraction of the frame period the shutter is open, in
`[0, 1]`; 0 is an instantaneous sample, 0.5 a 180-degree shutter.
`samples`: instants integrated per open exposure (midpoint rule);
`None` means 1 when `exposure == 0` and
[`DEFAULT_EXPOSURE_SAMPLES`](_autosummary/an.frame_clock.html.md#an.frame_clock.DEFAULT_EXPOSURE_SAMPLES) otherwise.
`jitter_sd`: standard deviation, in seconds, of each frame’s CAPTURE
offset from the nominal grid — the frame really is taken early or late.
Gaussian, clamped at [`max_jitter`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock.max_jitter) so frames never overlap; a
`jitter_sd` above half that clamp is refused rather than silently
shrunk (at `exposure=1` there is no room for any).
`phase`: seconds added to every capture instant — the camera clock’s
sub-frame offset from scene time, in `(-1/fps, 1/fps)`.
`timestamps`: the base of `CapturedFrame.t_reported` —
`"nominal"` (`index / fps`, what a naive tick loop or an mp4 reports)
or `"actual"` (`t_open`, what a capture API with real timestamps
reports).
`report_noise_sd`: Gaussian noise, in seconds, added to the REPORTED
timestamp only — regular capture, noisy clock (a browser frame callback).
Clamped at [`MAX_REPORT_NOISE_FRACTION`](_autosummary/an.frame_clock.html.md#an.frame_clock.MAX_REPORT_NOISE_FRACTION) of a frame period, with the
same refuse-rather-than-shrink rule as `jitter_sd`, and reported
timestamps must still increase: a real frame clock never runs backwards.
`seed`: the random streams (capture jitter and report noise are
independent draws from it).

Every instant is clipped into `[0, duration]`, because a render cannot
sample a scene outside its own timeline; the clipped values are what
[`frames()`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock.frames) returns and what gets rendered.

#### frames(duration)

One [`CapturedFrame`](_autosummary/an.frame_clock.html.md#an.frame_clock.CapturedFrame) per output frame of a `duration` render.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`CapturedFrame`](_autosummary/an.frame_clock.html.md#an.frame_clock.CapturedFrame), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

#### *property* max_jitter *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The clamp on a frame’s capture offset, in seconds.

```pycon
>>> round(FrameClock(fps=10, exposure=0.5).max_jitter, 9)
0.0245
```

#### sample_times(duration)

Per frame, the scene instants to render and average — the render seam.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

#### *property* samples_per_frame *: [int](https://docs.python.org/3/builtins/functions.html#int)*

1 for an instantaneous shutter.

```pycon
>>> FrameClock().samples_per_frame, FrameClock(exposure=0.5).samples_per_frame
(1, 8)
```

* **Type:**
  The resolved sample count

### *exception* an.frame_clock.FrameClockError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A frame clock that cannot describe a camera.

### an.frame_clock.MAX_JITTER_FRACTION *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.49*

A frame’s capture offset is clamped to this fraction of the slack between
one exposure closing and the next opening, on each side. Below one half, so
two neighbouring frames can each move toward the other by the full clamp and
still not overlap: the clock never reorders frames, whatever `jitter_sd` is.

### an.frame_clock.MAX_REPORT_NOISE_FRACTION *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.24*

Report noise is clamped to this fraction of the frame period, each side, so
two neighbouring reported timestamps can never cross on a regular grid.

### an.frame_clock.frame_count(duration, fps)

Frames in a render of `duration` seconds — the renderer’s own rule.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> frame_count(2.0, 30), frame_count(0.01, 30)
(60, 1)
```


# _autosummary/an.genres.html.md

# 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`](_autosummary/an.genres.html.md#an.genres.Genre) object — the shape `shaping` uses (“a new genre is
one file”) — listing what it registers:

- **action kinds** ([`ActionKind`](_autosummary/an.genres.html.md#an.genres.ActionKind)): a model, how it occupies time, how
  `scene.md` spells it;
- **entity kinds** ([`EntityKind`](_autosummary/an.genres.html.md#an.genres.EntityKind)), each naming the property space its
  nodes live in, plus any new **property spaces** and **field kinds** for the
  timing kernel ([`an.timing`](_autosummary/an.timing.html.md#module-an.timing));
- **semantic checks** ([`SemanticCheck`](_autosummary/an.genres.html.md#an.genres.SemanticCheck)) that `an validate` runs;
- **md sugar** ([`DialogueSugar`](_autosummary/an.genres.html.md#an.genres.DialogueSugar)) on `scene.md` dialogue lines;
- **capabilities** and **analysers** ([`an.capabilities`](_autosummary/an.capabilities.html.md#module-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`](_autosummary/an.semantic.html.md#module-an.semantic), ADR 0003).

Because the object is plain data, a genre is \*\*inspectable before it is
loaded\*\*: [`available()`](_autosummary/an.genres.html.md#an.genres.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()`](_autosummary/an.genres.html.md#an.genres.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:

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

[`register_genre()`](_autosummary/an.genres.html.md#an.genres.register_genre) installs a genre object directly (a test, a notebook, a
genre defined in the same process).

```pycon
>>> 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`](_autosummary/an.genres.html.md#an.genres.ENTRY_POINT_GROUP)      | The entry-point group a genre package declares its [`Genre`](_autosummary/an.genres.html.md#an.genres.Genre) under.   |
|-------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
| [`API_LEVEL`](_autosummary/an.genres.html.md#an.genres.API_LEVEL)              | every hook, registry field and moved path a genre package may rely on.                                             |
| [`IN_DISTRIBUTION_GENRES`](_autosummary/an.genres.html.md#an.genres.IN_DISTRIBUTION_GENRES) | Genres the `an` distribution itself ships, as entry-point values.                                                  |

### Functions

| [`require_api_level`](_autosummary/an.genres.html.md#an.genres.require_api_level)(level, \*, package)            | Refuse, with an upgrade hint, when this `an` is older than `package` needs.                                                                                                                                            |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`hook_modules`](_autosummary/an.genres.html.md#an.genres.hook_modules)(\*[, exclude_owner])                | The modules whose code the genres' registered hooks run, sorted and unique.                                                                                                                                            |
| [`register_service`](_autosummary/an.genres.html.md#an.genres.register_service)(name, target, \*[, owner, ...]) | Register a named service a genre offers the core (an#225).                                                                                                                                                             |
| [`require_service`](_autosummary/an.genres.html.md#an.genres.require_service)(name, \*[, what, extra])         | The registered service `name`; a typed error naming the install when absent.                                                                                                                                           |
| [`service`](_autosummary/an.genres.html.md#an.genres.service)(name[, default])                         | The registered service `name`, resolved, or `default` when none is.                                                                                                                                                    |
| [`services`](_autosummary/an.genres.html.md#an.genres.services)(prefix)                                 | `{name without the prefix: resolved service}` for every service under `prefix`.                                                                                                                                        |
| [`action_kind`](_autosummary/an.genres.html.md#an.genres.action_kind)(name)                                | The registered kind called `name`, or `None`.                                                                                                                                                                          |
| [`action_kind_names`](_autosummary/an.genres.html.md#an.genres.action_kind_names)(\*[, owner])                   | Registered action-kind names in registration order; `owner`'s only when given.                                                                                                                                         |
| [`available`](_autosummary/an.genres.html.md#an.genres.available)(\*[, entry_points, builtin])           | Every discoverable genre's declaration, by name — read, never registered.                                                                                                                                              |
| `entity_kind`(name)                                                                               |                                                                                                                                                                                                                        |
| `entity_kind_names`(\*[, owner])                                                                  |                                                                                                                                                                                                                        |
| [`entity_space_resolver`](_autosummary/an.genres.html.md#an.genres.entity_space_resolver)(entities, \*[, default])   | `target -> PropertySpace`: the space its ENTITY's kind declares.                                                                                                                                                       |
| [`discovered_entry_points`](_autosummary/an.genres.html.md#an.genres.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`](_autosummary/an.genres.html.md#an.genres.genre_entry_points)(\*[, group])                  | The installed `an.genres` entry points (nothing is imported).                                                                                                                                                          |
| [`genres_declaring`](_autosummary/an.genres.html.md#an.genres.genres_declaring)(test)                           | The installed genres (loaded or not) whose declaration passes `test`.                                                                                                                                                  |
| [`installed`](_autosummary/an.genres.html.md#an.genres.installed)()                                      | The names of the genres registered in this process, in registration order.                                                                                                                                             |
| [`genre_library`](_autosummary/an.genres.html.md#an.genres.genre_library)(name, \*[, default])               | The package whose data root holds genre `name`'s library and projects.                                                                                                                                                 |
| `installed_genre`(name)                                                                           |                                                                                                                                                                                                                        |
| [`load`](_autosummary/an.genres.html.md#an.genres.load)(\*[, entry_points, builtin])                | Register every discoverable genre.                                                                                                                                                                                     |
| [`providers_of`](_autosummary/an.genres.html.md#an.genres.providers_of)(kind, \*[, registry])               | The installed genres (loaded or not) that declare `kind` in `registry` (a key of [`Genre.provides()`](_autosummary/an.genres.html.md#an.genres.Genre.provides)).                                                                   |
| [`register_genre`](_autosummary/an.genres.html.md#an.genres.register_genre)(genre, \*[, replace, ...])        | Register everything `genre` declares, owned by `genre.name`.                                                                                                                                                           |
| [`without_genres`](_autosummary/an.genres.html.md#an.genres.without_genres)()                                 | Run a block with no genre registered (the core alone), then restore.                                                                                                                                                   |

### Classes

| [`ActionKind`](_autosummary/an.genres.html.md#an.genres.ActionKind)(name, model[, duration, flatten, ...])   | One kind of action: its model, how it occupies time, how `scene.md` spells it.                                                                                                                                                                               |
|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`CompilePass`](_autosummary/an.genres.html.md#an.genres.CompilePass)(name, run[, order, compiler, ...])      | One step a genre adds to an engine's COMPILER (shot -> compiled document).                                                                                                                                                                                   |
| [`RuntimeScript`](_autosummary/an.genres.html.md#an.genres.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`](_autosummary/an.genres.html.md#an.genres.SwapDeclaration)(sets[, descriptor, ...])            | What one entity's descriptor declares for the stage compiler's swap vocabulary.                                                                                                                                                                              |
| [`DialogueSugar`](_autosummary/an.genres.html.md#an.genres.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`](_autosummary/an.genres.html.md#an.genres.EntityKind)(name[, space, store, ...])               | One kind of entity (`AssetRef.kind`): what its nodes' properties are.                                                                                                                                                                                        |
| [`Genre`](_autosummary/an.genres.html.md#an.genres.Genre)(name[, title, description, package, ...])     | A genre: one plain, declarative object listing what it registers.                                                                                                                                                                                            |
| [`SemanticCheck`](_autosummary/an.genres.html.md#an.genres.SemanticCheck)(name, run[, stage, order, ...])       | One semantic-validation check: `run(ctx)` adds findings to `ctx.report`.                                                                                                                                                                                     |

### Exceptions

| [`GenreAPILevelError`](_autosummary/an.genres.html.md#an.genres.GenreAPILevelError)                           | A genre package needs a newer `an` than the one installed.             |
|-----------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`ServiceMissingError`](_autosummary/an.genres.html.md#an.genres.ServiceMissingError)                          | The core asked a genre for a service nobody registered.                |
| [`GenreError`](_autosummary/an.genres.html.md#an.genres.GenreError)                                   | A genre declaration is malformed, or its entry point does not resolve. |
| [`RegistryError`](_autosummary/an.genres.html.md#an.genres.RegistryError)                                | A registration is malformed or collides with one already made.         |
| [`UnregisteredKindError`](_autosummary/an.genres.html.md#an.genres.UnregisteredKindError)(what, name, \*[, ...]) | A document names a kind no loaded genre registered.                    |

### an.genres.API_LEVEL *: [int](https://docs.python.org/3/builtins/functions.html#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()`](_autosummary/an.genres.html.md#an.genres.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()`](_autosummary/an.genres.html.md#an.genres.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)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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`](_autosummary/an.ir.schema.html.md#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`](_autosummary/an.ir.compose.html.md#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](https://docs.python.org/3/library/typing.html#typing.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`](_autosummary/an.stage.compile.html.md#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](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

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

#### version *: [str](https://docs.python.org/3/builtins/stdtypes.html#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`](_autosummary/an.semantic.html.md#module-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)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

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

A compiler (`compiler`: today `"stage"`, [`an.stage.compile`](_autosummary/an.stage.compile.html.md#module-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()`](_autosummary/an.genres.html.md#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](https://docs.python.org/3/builtins/functions.html#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()

The callable `run` names.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> CompilePass("p", "math:sqrt").resolve()(4.0)
2.0
```

### *class* an.genres.DialogueSugar(name, opener, field, parse, format, description='', fields=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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)

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

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.genres.ENTRY_POINT_GROUP *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an.genres'*

The entry-point group a genre package declares its [`Genre`](_autosummary/an.genres.html.md#an.genres.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)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

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

`space` names the registered [`an.timing.spaces.PropertySpace`](_autosummary/an.timing.spaces.html.md#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](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/builtins/functions.html#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](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[Any](https://docs.python.org/3/library/typing.html#typing.Any)], [Any](https://docs.python.org/3/library/typing.html#typing.Any)] | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/library/typing.html#typing.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](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Any](https://docs.python.org/3/library/typing.html#typing.Any)], [SwapDeclaration](_autosummary/an.genres.registry.html.md#an.genres.registry.SwapDeclaration) | [None](https://docs.python.org/3/builtins/constants.html#None)] | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '1'*

The kind’s vocabulary version (ADR 0003), as [`ActionKind.version`](_autosummary/an.genres.html.md#an.genres.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>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace) objects and
`field_kinds` are `(name, factory)` pairs for
[`an.timing.kinds.register_kind()`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.register_kind). `capabilities` are
[`an.capabilities.Capability`](_autosummary/an.capabilities.html.md#an.capabilities.Capability) objects, `analysers`
[`an.capabilities.Analyser`](_autosummary/an.capabilities.html.md#an.capabilities.Analyser) objects, `vocabulary`
[`an.semantic.Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry) objects (methods included) and `aspects`
[`an.semantic.Aspect`](_autosummary/an.semantic.html.md#an.semantic.Aspect) objects; registering the genre checks the whole
([`an.semantic.check_registry()`](_autosummary/an.semantic.html.md#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()`](_autosummary/an.genres.html.md#an.genres.genre_library)).

#### compile_passes *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CompilePass](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass), ...]* *= ()*

Steps the genre adds to an engine’s compiler, and builders for its
entity kinds ([`CompilePass`](_autosummary/an.genres.html.md#an.genres.CompilePass); an#247).

#### provides()

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

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]]

```pycon
>>> Genre("g", action_kinds=()).provides()["action kinds"]
()
```

#### runtime_scripts *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[RuntimeScript](_autosummary/an.genres.registry.html.md#an.genres.registry.RuntimeScript), ...]* *= ()*

its visual kinds
([`RuntimeScript`](_autosummary/an.genres.html.md#an.genres.RuntimeScript); an#247).

* **Type:**
  Code the genre adds to an engine’s runtime

#### services *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [object](https://docs.python.org/3/builtins/functions.html#object)]*

`{name: object or "module:attr"}` the genre offers the core by name
([`register_service()`](_autosummary/an.genres.html.md#an.genres.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

Bases: [`GenreError`](_autosummary/an.genres.html.md#an.genres.GenreError), [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)

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

### *exception* an.genres.GenreError

Bases: [`RegistryError`](_autosummary/an.genres.registry.html.md#an.genres.registry.RegistryError)

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

### an.genres.IN_DISTRIBUTION_GENRES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)], ...]* *= ()*

Genres the `an` distribution itself ships, as entry-point values. They are
found by [`load()`](_autosummary/an.genres.html.md#an.genres.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()`](_autosummary/an.genres.html.md#an.genres.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

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A registration is malformed or collides with one already made.

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

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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`](https://docs.python.org/3/library/importlib.resources.html#module-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()

The script’s code (UTF-8).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> RuntimeScript("x", "an.stage.runtime:extensions.js").code().startswith("//")
True
```

### *class* an.genres.SemanticCheck(name, run, stage='shot', order=0.0, description='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

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

`ctx` is [`an.ir.validate.ValidationContext`](_autosummary/an.ir.validate.html.md#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

Bases: [`RegistryError`](_autosummary/an.genres.registry.html.md#an.genres.registry.RegistryError), [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)

The core asked a genre for a service nobody registered.

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

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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='')

Bases: [`RegistryError`](_autosummary/an.genres.registry.html.md#an.genres.registry.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)

The registered kind called `name`, or `None`.

* **Return type:**
  [`ActionKind`](_autosummary/an.genres.registry.html.md#an.genres.registry.ActionKind) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.genres.action_kind_names(, owner=None)

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

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.available(, entry_points=None, builtin=True)

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

The in-distribution genres plus the entry points ([`discovered_entry_points()`](_autosummary/an.genres.html.md#an.genres.discovered_entry_points)).
A genre whose entry point does not import is left out (its error is what
[`load()`](_autosummary/an.genres.html.md#an.genres.load) raises).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Genre`](_autosummary/an.genres.html.md#an.genres.Genre)]

### an.genres.discovered_entry_points(, entry_points=None, builtin=True)

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`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`EntryPoint`](https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.EntryPoint), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> [ep.name for ep in discovered_entry_points(entry_points=())]
[]
```

### an.genres.entity_space_resolver(entities, , default=None)

`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`](_autosummary/an.timing.spaces.html.md#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.

```pycon
>>> 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')

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

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`EntryPoint`](https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.EntryPoint), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.genre_library(name, , default='an')

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()`](_autosummary/an.genres.html.md#an.genres.load) first, as every entry point does).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> genre_library(None)
'an'
```

### an.genres.genres_declaring(test)

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`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.hook_modules(, exclude_owner='an')

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`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> isinstance(hook_modules(), tuple)
True
```

### an.genres.installed()

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

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.load(, entry_points=None, builtin=True)

Register every discoverable genre. Idempotent.

Discoverable: the genres the `an` distribution ships
([`IN_DISTRIBUTION_GENRES`](_autosummary/an.genres.html.md#an.genres.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`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.providers_of(kind, , registry='action kinds')

The installed genres (loaded or not) that declare `kind` in `registry`
(a key of [`Genre.provides()`](_autosummary/an.genres.html.md#an.genres.Genre.provides)). Used by the unregistered-kind errors.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.register_genre(genre, , replace=False, check_capabilities=True)

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()`](_autosummary/an.genres.html.md#an.genres.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()`](_autosummary/an.genres.html.md#an.genres.load) runs it once all
genres are in, so a genre extending another loads in any order).

* **Return type:**
  [`Genre`](_autosummary/an.genres.html.md#an.genres.Genre)

### an.genres.register_service(name, target, , owner='an', replace=False)

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`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.genres.require_api_level(level, , package)

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`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> 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')

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

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

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

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

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> service("no.such.service") is None
True
```

### an.genres.services(prefix)

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

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> services("no.such.")
{}
```

### an.genres.without_genres()

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

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

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

### Modules

| [`registry`](_autosummary/an.genres.registry.html.md#module-an.genres.registry)   | The core's open registries: action kinds, entity kinds, semantic checks, md sugar, compile passes.   |
|---------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|


# _autosummary/an.genres.registry.html.md

# an.genres.registry

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

ADR 0001 decision 2 (“the IR is open at the type level; genres register, never
edit”) and decision 4 (the first batch of registries). Each registry is a plain
name-keyed table with an **owner** per entry, so the core can tell its own
entries from a genre’s (the timing contract does the same, review-237 S3), and a
test can take every genre out and put it back ([`snapshot()`](_autosummary/an.genres.registry.html.md#an.genres.registry.snapshot) /
`restore()`).

This module imports nothing from `an.ir`: the schema consults it while it
validates (a document’s `kind: play` is looked up here), so it must sit below
the schema in the import graph. That is also why an entry holds its model class
and hooks as plain values: the registry never needs to know what they are.

```pycon
>>> isinstance(action_kind("tween"), ActionKind)  # core kinds register on import of the IR
True
>>> "play" in action_kind_names(owner=CORE_OWNER)  # a genre's kind is never the core's
False
```

### Module Attributes

| [`CORE_OWNER`](_autosummary/an.genres.registry.html.md#an.genres.registry.CORE_OWNER)        | The owner of every entry the core registers itself.                |
|--------------------------------------------------------------------|--------------------------------------------------------------------|
| [`DurationHook`](_autosummary/an.genres.registry.html.md#an.genres.registry.DurationHook)      | the natural length of a leaf.                                      |
| [`CheckStage`](_autosummary/an.genres.registry.html.md#an.genres.registry.CheckStage)        | once before the shots, once per shot, once after them.             |
| [`DIALOGUE_BRACKETS`](_autosummary/an.genres.registry.html.md#an.genres.registry.DIALOGUE_BRACKETS) | The bracket pairs a GENRE may claim on a `scene.md` dialogue line. |

### Functions

| [`action_kind`](_autosummary/an.genres.registry.html.md#an.genres.registry.action_kind)(name)                                 | The registered kind called `name`, or `None`.                                     |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`action_kind_names`](_autosummary/an.genres.registry.html.md#an.genres.registry.action_kind_names)(\*[, owner])                    | Registered action-kind names in registration order; `owner`'s only when given.    |
| `action_kind_owner`(name)                                                                          |                                                                                   |
| `check_names`(\*[, owner])                                                                         |                                                                                   |
| [`check_owner`](_autosummary/an.genres.registry.html.md#an.genres.registry.check_owner)(name)                                 | Who registered the check `name` (`CORE_OWNER` for the core's), or `None`.         |
| [`checks`](_autosummary/an.genres.registry.html.md#an.genres.registry.checks)(stage)                                     | The registered checks of `stage`, in run order (`order`, then registration).      |
| `compile_pass_names`(\*[, owner])                                                                  |                                                                                   |
| [`compile_pass_owner`](_autosummary/an.genres.registry.html.md#an.genres.registry.compile_pass_owner)(name)                          | Who registered the compile pass `name` (a genre's name), or `None`.               |
| [`compile_passes`](_autosummary/an.genres.registry.html.md#an.genres.registry.compile_passes)(compiler)                          | The registered passes of `compiler` (builders excluded), in run order.            |
| [`dialogue_sugar`](_autosummary/an.genres.registry.html.md#an.genres.registry.dialogue_sugar)(opener)                            | The sugar registered for the bracket `opener`, or `None`.                         |
| `dialogue_sugars`()                                                                                |                                                                                   |
| [`entity_builders`](_autosummary/an.genres.registry.html.md#an.genres.registry.entity_builders)(compiler)                         | `{entity kind: builder}` registered for `compiler`.                               |
| `entity_kind`(name)                                                                                |                                                                                   |
| `entity_kind_names`(\*[, owner])                                                                   |                                                                                   |
| [`hook_modules`](_autosummary/an.genres.registry.html.md#an.genres.registry.hook_modules)(\*[, exclude_owner])                 | The modules whose code the genres' registered hooks run, sorted and unique.       |
| [`owners`](_autosummary/an.genres.registry.html.md#an.genres.registry.owners)()                                          | Every owner with at least one entry, the core first.                              |
| [`register_action_kind`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_action_kind)(kind, \*[, owner, replace])  | Register an action kind.                                                          |
| [`register_check`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_check)(check, \*[, owner, replace])       | Register a semantic-validation check (run by `an.ir.validate.validate_semantic`). |
| [`register_compile_pass`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_compile_pass)(compile_pass, \*[, ...])    | Register a compile pass (or an entity builder) under its name.                    |
| [`register_dialogue_sugar`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_dialogue_sugar)(sugar, \*[, owner, ...])  | Register `scene.md` dialogue sugar.                                               |
| [`register_entity_kind`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_entity_kind)(kind, \*[, owner, replace])  | Register an entity kind (a value `AssetRef.kind` may take).                       |
| [`register_runtime_script`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_runtime_script)(script, \*[, owner, ...]) | Register runtime code for an engine (a genre's visual kinds).                     |
| [`register_service`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_service)(name, target, \*[, owner, ...])  | Register a named service a genre offers the core (an#225).                        |
| [`require_service`](_autosummary/an.genres.registry.html.md#an.genres.registry.require_service)(name, \*[, what, extra])          | The registered service `name`; a typed error naming the install when absent.      |
| `restore`(state)                                                                                   |                                                                                   |
| [`runtime_scripts`](_autosummary/an.genres.registry.html.md#an.genres.registry.runtime_scripts)(engine)                           | The scripts registered for `engine`, by name (a stable order).                    |
| [`service`](_autosummary/an.genres.registry.html.md#an.genres.registry.service)(name[, default])                          | The registered service `name`, resolved, or `default` when none is.               |
| [`services`](_autosummary/an.genres.registry.html.md#an.genres.registry.services)(prefix)                                  | `{name without the prefix: resolved service}` for every service under `prefix`.   |
| [`snapshot`](_autosummary/an.genres.registry.html.md#an.genres.registry.snapshot)()                                        | The state of every table, for `restore()`.                                        |
| [`unregister_owner`](_autosummary/an.genres.registry.html.md#an.genres.registry.unregister_owner)(owner)                           | Remove every entry `owner` registered, from every table.                          |

### Classes

| [`ActionKind`](_autosummary/an.genres.registry.html.md#an.genres.registry.ActionKind)(name, model[, duration, flatten, ...])   | One kind of action: its model, how it occupies time, how `scene.md` spells it.                                                                                                                                                                               |
|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`CompilePass`](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass)(name, run[, order, compiler, ...])      | One step a genre adds to an engine's COMPILER (shot -> compiled document).                                                                                                                                                                                   |
| [`DialogueSugar`](_autosummary/an.genres.registry.html.md#an.genres.registry.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`](_autosummary/an.genres.registry.html.md#an.genres.registry.EntityKind)(name[, space, store, ...])               | One kind of entity (`AssetRef.kind`): what its nodes' properties are.                                                                                                                                                                                        |
| [`RuntimeScript`](_autosummary/an.genres.registry.html.md#an.genres.registry.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). |
| [`SemanticCheck`](_autosummary/an.genres.registry.html.md#an.genres.registry.SemanticCheck)(name, run[, stage, order, ...])       | One semantic-validation check: `run(ctx)` adds findings to `ctx.report`.                                                                                                                                                                                     |
| [`SwapDeclaration`](_autosummary/an.genres.registry.html.md#an.genres.registry.SwapDeclaration)(sets[, descriptor, ...])            | What one entity's descriptor declares for the stage compiler's swap vocabulary.                                                                                                                                                                              |

### Exceptions

| [`RegistryError`](_autosummary/an.genres.registry.html.md#an.genres.registry.RegistryError)                                | A registration is malformed or collides with one already made.   |
|-----------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [`ServiceMissingError`](_autosummary/an.genres.registry.html.md#an.genres.registry.ServiceMissingError)                          | The core asked a genre for a service nobody registered.          |
| [`UnregisteredKindError`](_autosummary/an.genres.registry.html.md#an.genres.registry.UnregisteredKindError)(what, name, \*[, ...]) | A document names a kind no loaded genre registered.              |

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

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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`](_autosummary/an.ir.schema.html.md#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`](_autosummary/an.ir.compose.html.md#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](https://docs.python.org/3/library/typing.html#typing.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`](_autosummary/an.stage.compile.html.md#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](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

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

#### version *: [str](https://docs.python.org/3/builtins/stdtypes.html#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`](_autosummary/an.semantic.html.md#module-an.semantic) folds it into the shot digest).

* **Type:**
  The kind’s vocabulary version (ADR 0003)

### an.genres.registry.CORE_OWNER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an'*

The owner of every entry the core registers itself. A genre registers under
its own name (`an.genres.Genre.name`).

### an.genres.registry.CheckStage

once before the shots, once per shot, once after them.

* **Type:**
  When a check runs

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘scene’, ‘shot’, ‘finish’]

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

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

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

A compiler (`compiler`: today `"stage"`, [`an.stage.compile`](_autosummary/an.stage.compile.html.md#module-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()`](_autosummary/an.genres.html.md#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](https://docs.python.org/3/builtins/functions.html#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()

The callable `run` names.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> CompilePass("p", "math:sqrt").resolve()(4.0)
2.0
```

### an.genres.registry.DIALOGUE_BRACKETS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'[': ']'}*

The bracket pairs a GENRE may claim on a `scene.md` dialogue line. The
line grammar has three: `(…)` (timing) and `{…}` (delivery direction)
are the core’s own and never registered; `[…]` is the one left for sugar.

### *class* an.genres.registry.DialogueSugar(name, opener, field, parse, format, description='', fields=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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)

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

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.genres.registry.DurationHook

the natural length of a leaf. `extent` is
the caller’s per-call resolver for a leaf that has no explicit duration (the
compiler and `an validate` pass one bound to the entity’s descriptor), or
`None`.

* **Type:**
  `(action, extent) -> seconds`

alias of [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]

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

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

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

`space` names the registered [`an.timing.spaces.PropertySpace`](_autosummary/an.timing.spaces.html.md#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](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/builtins/functions.html#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](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[Any](https://docs.python.org/3/library/typing.html#typing.Any)], [Any](https://docs.python.org/3/library/typing.html#typing.Any)] | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/library/typing.html#typing.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](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Any](https://docs.python.org/3/library/typing.html#typing.Any)], [SwapDeclaration](_autosummary/an.genres.registry.html.md#an.genres.registry.SwapDeclaration) | [None](https://docs.python.org/3/builtins/constants.html#None)] | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '1'*

The kind’s vocabulary version (ADR 0003), as [`ActionKind.version`](_autosummary/an.genres.registry.html.md#an.genres.registry.ActionKind.version).

### *exception* an.genres.registry.RegistryError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A registration is malformed or collides with one already made.

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

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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`](https://docs.python.org/3/library/importlib.resources.html#module-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()

The script’s code (UTF-8).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> RuntimeScript("x", "an.stage.runtime:extensions.js").code().startswith("//")
True
```

### *class* an.genres.registry.SemanticCheck(name, run, stage='shot', order=0.0, description='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

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

`ctx` is [`an.ir.validate.ValidationContext`](_autosummary/an.ir.validate.html.md#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.registry.ServiceMissingError

Bases: [`RegistryError`](_autosummary/an.genres.registry.html.md#an.genres.registry.RegistryError), [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)

The core asked a genre for a service nobody registered.

### *class* an.genres.registry.SwapDeclaration(sets, descriptor=None, art_exists=None, scale=1.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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.registry.UnregisteredKindError(what, name, , known=(), providers=(), where='')

Bases: [`RegistryError`](_autosummary/an.genres.registry.html.md#an.genres.registry.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.registry.action_kind(name)

The registered kind called `name`, or `None`.

* **Return type:**
  [`ActionKind`](_autosummary/an.genres.registry.html.md#an.genres.registry.ActionKind) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.genres.registry.action_kind_names(, owner=None)

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

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.registry.check_owner(name)

Who registered the check `name` (`CORE_OWNER` for the core’s), or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.genres.registry.checks(stage)

The registered checks of `stage`, in run order (`order`, then registration).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`SemanticCheck`](_autosummary/an.genres.registry.html.md#an.genres.registry.SemanticCheck), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.registry.compile_pass_owner(name)

Who registered the compile pass `name` (a genre’s name), or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.genres.registry.compile_passes(compiler)

The registered passes of `compiler` (builders excluded), in run order.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`CompilePass`](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.registry.dialogue_sugar(opener)

The sugar registered for the bracket `opener`, or `None`.

* **Return type:**
  [`DialogueSugar`](_autosummary/an.genres.registry.html.md#an.genres.registry.DialogueSugar) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.genres.registry.entity_builders(compiler)

`{entity kind: builder}` registered for `compiler`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`CompilePass`](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass)]

### an.genres.registry.hook_modules(, exclude_owner='an')

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`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> isinstance(hook_modules(), tuple)
True
```

### an.genres.registry.owners()

Every owner with at least one entry, the core first.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.registry.register_action_kind(kind, , owner='an', replace=False)

Register an action kind. Its model’s `kind` must default to its name.

* **Return type:**
  [`ActionKind`](_autosummary/an.genres.registry.html.md#an.genres.registry.ActionKind)

```pycon
>>> from pydantic import BaseModel
>>> class Bad(BaseModel):
...     kind: str = "other"
>>> register_action_kind(ActionKind("wave", Bad), owner="demo")
Traceback (most recent call last):
...
an.genres.registry.RegistryError: action kind 'wave': ...
```

### an.genres.registry.register_check(check, , owner='an', replace=False)

Register a semantic-validation check (run by `an.ir.validate.validate_semantic`).

* **Return type:**
  [`SemanticCheck`](_autosummary/an.genres.registry.html.md#an.genres.registry.SemanticCheck)

### an.genres.registry.register_compile_pass(compile_pass, , owner='an', replace=False)

Register a compile pass (or an entity builder) under its name.

* **Return type:**
  [`CompilePass`](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass)

### an.genres.registry.register_dialogue_sugar(sugar, , owner='an', replace=False)

Register `scene.md` dialogue sugar. One sugar per bracket pair.

* **Return type:**
  [`DialogueSugar`](_autosummary/an.genres.registry.html.md#an.genres.registry.DialogueSugar)

### an.genres.registry.register_entity_kind(kind, , owner='an', replace=False)

Register an entity kind (a value `AssetRef.kind` may take).

* **Return type:**
  [`EntityKind`](_autosummary/an.genres.registry.html.md#an.genres.registry.EntityKind)

### an.genres.registry.register_runtime_script(script, , owner='an', replace=False)

Register runtime code for an engine (a genre’s visual kinds).

* **Return type:**
  [`RuntimeScript`](_autosummary/an.genres.registry.html.md#an.genres.registry.RuntimeScript)

### an.genres.registry.register_service(name, target, , owner='an', replace=False)

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`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.genres.registry.require_service(name, , what='', extra='cutout')

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

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.genres.registry.runtime_scripts(engine)

The scripts registered for `engine`, by name (a stable order).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`RuntimeScript`](_autosummary/an.genres.registry.html.md#an.genres.registry.RuntimeScript), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.genres.registry.service(name, default=None)

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

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> service("no.such.service") is None
True
```

### an.genres.registry.services(prefix)

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

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> services("no.such.")
{}
```

### an.genres.registry.snapshot()

The state of every table, for `restore()`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)

### an.genres.registry.unregister_owner(owner)

Remove every entry `owner` registered, from every table.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.html.md

# 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.

```pycon
>>> import an
>>> 'SceneIR' in an.__all__
True
```

### Functions

| [`set_`](_autosummary/an.html.md#an.set_)(target, property, value, \*[, at])          | Discrete property set at time `at` (relative to its enclosing scope).          |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`tween`](_autosummary/an.html.md#an.tween)(target, property, to, duration, \*[, ...]) | Animate a property from `from_` (or its current value) to `to`.                |
| [`sequence`](_autosummary/an.html.md#an.sequence)(\*actions)                              | Run children one after the other.                                              |
| [`parallel`](_autosummary/an.html.md#an.parallel)(\*actions)                              | Run all children at once.                                                      |
| [`stagger`](_autosummary/an.html.md#an.stagger)(lag, \*actions)                          | Start each action `lag` seconds after the previous one STARTS.                 |
| [`crowd`](_autosummary/an.html.md#an.crowd)(id, \*, ref, count[, kind, store, ...])    | `count` entities of one asset (`kind`/`store`/`ref`) placed in `area`.         |
| [`fan_out`](_autosummary/an.html.md#an.fan_out)(action, members, \*[, crowd_id])         | A copy of `action` per member, its targets moved from the crowd to the member. |
| [`delay`](_autosummary/an.html.md#an.delay)(duration)                                  | An empty span that consumes time.                                              |
| [`loop`](_autosummary/an.html.md#an.loop)(action, count)                              | Repeat `action` `count` times.                                                 |
| [`flatten`](_autosummary/an.html.md#an.flatten)(action, \*[, start, play_extent])        | Walk a composition tree, emitting leaf actions with absolute times.            |
| [`validate_schema`](_autosummary/an.html.md#an.validate_schema)(doc)                             | Validate that `doc` (dict, JSON string, or SceneIR) conforms to the schema.    |
| [`validate_semantic`](_autosummary/an.html.md#an.validate_semantic)(scene, \*[, ...])              | Cross-field semantic checks.                                                   |
| [`markdown_to_ir`](_autosummary/an.html.md#an.markdown_to_ir)(md_text)                          | Parse the structured Markdown form of a scene into a SceneIR.                  |
| [`ir_to_markdown`](_autosummary/an.html.md#an.ir_to_markdown)(scene)                            | Render a SceneIR back into the structured Markdown form.                       |
| [`init`](_autosummary/an.html.md#an.init)(project_dir, \*[, name, force])             | Create a fresh an project at `project_dir`.                                    |
| [`load`](_autosummary/an.html.md#an.load)(project_dir, \*[, check_kinds])             | Load an existing project.                                                      |
| [`save`](_autosummary/an.html.md#an.save)(project)                                    | Persist a Project's current scene back to disk (md + json).                    |
| [`build_project_mall`](_autosummary/an.html.md#an.build_project_mall)(project_dir, \*[, ensure])    | Build the standard project mall over `project_dir`.                            |
| [`check_requirements`](_autosummary/an.html.md#an.check_requirements)()                             | Return a per-tool status dict.                                                 |

### Classes

| [`SceneIR`](_autosummary/an.html.md#an.SceneIR)(\*\*data)              | Top-level Scene IR document.                             |
|---------------------------------------------------------------------------------|----------------------------------------------------------|
| [`Meta`](_autosummary/an.html.md#an.Meta)(\*\*data)                 | Scene metadata.                                          |
| [`AssetRef`](_autosummary/an.html.md#an.AssetRef)(\*\*data)             | Reference to an entry in a project store.                |
| [`Shot`](_autosummary/an.html.md#an.Shot)(\*\*data)                 | A single rendered unit.                                  |
| [`Dialogue`](_autosummary/an.html.md#an.Dialogue)(\*\*data)             | One line of spoken dialogue.                             |
| [`Camera`](_autosummary/an.html.md#an.Camera)(\*\*data)               | Camera state for a shot: a named move, or explicit keys. |
| [`Resolution`](_autosummary/an.html.md#an.Resolution)(\*\*data)           | Pixel dimensions of the rendered output.                 |
| [`FlatAction`](_autosummary/an.html.md#an.FlatAction)(start, end, action) | A leaf action with its absolute start and end times.     |
| [`Project`](_autosummary/an.html.md#an.Project)(root, mall, scene)     | A loaded an project: directory + mall + current scene.   |

### *class* an.AssetRef(\*\*data)

Bases: `_IRModel`

Reference 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. `overrides` lets a single shot tweak presentation without
forking the asset.

```pycon
>>> AssetRef(kind="character", id="maya", store="characters", ref="maya-v1").id
'maya'
```

#### kind *: [str](https://docs.python.org/3/builtins/stdtypes.html#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 `str` in the schema, not a `Literal` (ADR 0001 decision 2): the
values are the REGISTERED entity kinds ([`an.genres`](_autosummary/an.genres.html.md#module-an.genres)) — the core’s
`environment`, `prop` and `voice`, a genre’s `character` — and
`an validate` checks it against that registry, naming the genre that
provides an unregistered one.

* **Type:**
  `"style"` was retired in an#106

#### library *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#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()`](_autosummary/an.library.ids.html.md#an.library.ids.parse_ref), a pinned version required: `vNNN` or
`sha256:<prefix>`, never `latest`). `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, so
`ref` still 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 unset `library`
leaves no trace in a dump, so no stored scene changes and no schema
version moves.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### stage *: [StagePlacement](_autosummary/an.ir.schema.html.md#an.ir.schema.StagePlacement) | [None](https://docs.python.org/3/builtins/constants.html#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 `AssetRef` never reaches
it. So this field can grow without retiring a single ledger row.

### *class* an.Camera(\*\*data)

Bases: `_CameraShakes`

Camera state for a shot: a named move, or explicit keys.

```pycon
>>> 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 named `move` desugars to a key list; `keys` is 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.

`keys` defaults to `None`, not `[]`, and that too is load-bearing: the
markdown writer dumps the camera with `exclude_none=True`, which KEEPS
empty lists — an empty default would write `keys: []` into every camera
block it regenerates.

`position`, `target` and `focal_length` were removed in an#109. They were
written into every `scene.md` this package ever generated and read by
nothing; a registered migration drops them.

#### keys *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[CameraKey](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraKey)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The explicit door. `None` = use `move`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### move *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

A named preset — sugar for `keys`. The cutout renderer’s vocabulary is
`an.stage.compile.CAMERA_MOVES`; validate and the compiler are
pinned to the same table by test, because a move that validates and then
raises is the failure `_check_renderable` exists to prevent.

### *class* an.Dialogue(\*\*data)

Bases: `_IRModel`

One line of spoken dialogue.

`start` and `duration` are None until the audio pipeline runs (TTS
gives us a real duration); the pipeline stamps them then, deriving
`start` from the author’s `pause` / `at` ([`planned_start()`](_autosummary/an.html.md#an.Dialogue.planned_start)).

#### at *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Where this line starts, in SHOT seconds, whatever came before it —
`(at 3.0)` in `scene.md`. `start` is what the audio pipeline
DERIVES from `at`/`pause` on every pass; these two are what the
author wrote (an#187).

#### direction *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

How the line is DELIVERED — cues such as `["excited"]` or
`["sighs", "annoyed"]`, `{excited}` in `scene.md` (an#209). A TTS
model that takes inline audio tags (ElevenLabs v3/v4) receives them as
`[excited] Hi!`; others ignore them. Never part of `text`, so
captions and lip-sync alignment never see a cue.

#### emotion_intensity *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`maya [angry 0.4]: …`
in `scene.md`, a typed parameter on the (b-name) emotion. `None` is
full strength, and is omitted from JSON.

* **Type:**
  How strongly `emotion` shows, 0..1 (an#253)

#### leveled *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None)*

<the synthesized
line’s audio_ref>, “gain_db”: <its voice’s gain>}\`\` when `audio_ref`
is the leveled audio. `None` — unleveled — is omitted from JSON.

* **Type:**
  Stamped by voice leveling (an#315)
* **Type:**
  ```
  ``
  ```

  {“source”

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### pause *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Seconds of silence before this line, after the previous line’s speech
ends (the shot start, for the first line) — `(pause 1.5)` in `scene.md`.

#### planned_start(cursor)

Where this line starts, given the previous line ends at `cursor`.

The one rule the audio pipeline stamps into `start` and `an validate`
lays lines out by: `at` if set, else `cursor + pause`. A `start`
on a line never synthesized (no `audio_ref`) was authored — the
spelling of `at` before an#187 — and counts as one; a synthesized
line’s `start` is the pipeline’s own stamp, re-derived here.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> 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](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

seconds from the line’s start to the end of its
AUDIBLE speech — its take’s own trailing silence left out — stamped
with `duration` from the line’s audio. The next line’s `pause` counts
from here (a line with no pause still follows the whole take).
`None` (silent audio, a line its voice’s `trim_silence` already cut to
the tail the author keeps, a line stamped before an#397) counts from the
end of `duration`, 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), so `scene.json` and its fixtures do not change.

* **Type:**
  DERIVED (an#397)

#### word_timings *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[WordTimingIR](_autosummary/an.ir.schema.html.md#an.ir.schema.WordTimingIR)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The provider’s word timings, line-relative; `None` when the provider
has none (offline, Rhubarb) or the line was stamped before an#96.

### *class* an.FlatAction(start, end, action)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A 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)

Bases: `_IRModel`

Scene metadata.

#### captions *: [Captions](_autosummary/an.ir.schema.html.md#an.ir.schema.Captions) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Captions from the dialogue’s word timings (`Captions`, an#175);
`None` — the default — is none, omitted from JSON like `style_pack`.

#### closing_transition *: [Transition](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition) | [None](https://docs.python.org/3/builtins/constants.html#None)*

a `fade` to its `color` over its
`duration`, 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 a `cut`) 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](https://docs.python.org/3/builtins/stdtypes.html#str) | [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The easing every authored `tween` that 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 like `style_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, `play` clips and swap channels have none to
inherit. There is no per-shot override yet — style is a scene’s.

#### default_renderer *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

a registered renderer’s name.

* **Type:**
  Like [`Shot.renderer`](_autosummary/an.html.md#an.Shot.renderer)

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### sounds *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[SoundCue](_autosummary/an.ir.schema.html.md#an.ir.schema.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](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Stepped timing for AUTHORED TWEENS, in pose updates per second; `None`
(the default) leaves every tween smooth. At 30 fps, `15` is “on twos”
and `10` “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 and `play` clips: only `tween` curves 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 (a `set` at an off-grid `at`
likewise lands where it was authored). A shot’s own `step_hz`
overrides this. Must be positive (schema) and `<= fps` (validate +
compile), an#89.

#### style_pack *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The `StylePack` in the project’s `styles` store 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 by
`an 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](_autosummary/an.ir.schema.html.md#an.ir.schema.VoiceLoudness) | [None](https://docs.python.org/3/builtins/constants.html#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)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A loaded an project: directory + mall + current scene.

### *class* an.Resolution(\*\*data)

Bases: `_IRModel`

Pixel dimensions of the rendered output.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.SceneIR(\*\*data)

Bases: `_IRModel`

Top-level Scene IR document. The SSOT.

A document is portable, diffable, and renderer-agnostic. Persisted as JSON
at `ir/scene.json` inside an an project.

```pycon
>>> 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'
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.Shot(\*\*data)

Bases: `_IRModel`

A single rendered unit. A scene is a sequence of shots.

A shot’s `renderer` selects the backend that draws it. Every renderer must accept the
same Shot fields; renderer-specific options go under `options`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### policy *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/builtins/stdtypes.html#str)*

Which RENDERER draws this shot — not art direction. The field was
called `style` until an#106, colliding with the styles store (which
holds art direction) and with `AssetRef(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.
A `str` in the schema (ADR 0001 decision 2): any name a renderer
registered (`an.adapters.register_renderer`), checked by `an
validate`. `cutout` stays the persisted default (decision 9).

#### sounds *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[SoundCue](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue)]*

Sound cues in SHOT-local time (`SoundCue`).

#### step_hz *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per-shot override of [`Meta.step_hz`](_autosummary/an.html.md#an.Meta.step_hz) (`None` = inherit).

#### transition *: [Transition](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition) | [None](https://docs.python.org/3/builtins/constants.html#None)*

How this shot is entered (`Transition`); `None` is a hard cut.

### an.build_project_mall(project_dir, , ensure=False, \*\*overrides)

Build the standard project mall over `project_dir`.

Pass `ensure=True` to create the per-store directories on disk if they
don’t exist. Pass keyword overrides to swap in alternate stores (e.g. an
in-memory `dict` for tests).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`MutableMapping`](https://docs.python.org/3/library/typing.html#typing.MutableMapping)]

### an.check_requirements()

Return a per-tool status dict.

The CLI subcommand `an check` pretty-prints this. Programmatic callers
can inspect the dict directly.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### 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)

`count` entities of one asset (`kind`/`store`/`ref`) placed in `area`.

id: the crowd’s name; member `k` is `<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 as `character`)
store: the store the asset lives in (default: the kind’s registered store)
layout: `grid` (rows filled from the back), `row` or `scatter`
area: `(x0, y0, x1, y1)` in scene pixels about the stage centre; a

> member’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

```pycon
>>> [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
```

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`AssetRef`](_autosummary/an.ir.schema.html.md#an.ir.schema.AssetRef)]

### an.delay(duration)

An empty span that consumes time. Useful inside `sequence`.

* **Return type:**
  [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction)

### an.fan_out(action, members, , crowd_id=None)

A copy of `action` per member, its targets moved from the crowd to the member.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

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 (`AssetRef` s 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()`](_autosummary/an.ir.compose.html.md#an.ir.compose.stagger) for a ripple through the ranks,
or [`parallel()`](_autosummary/an.ir.compose.html.md#an.ir.compose.parallel) for the crowd moving as one.

```pycon
>>> 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)

Walk a composition tree, emitting leaf actions with absolute times.

A `play` without `duration` advances a `sequence` by `play_extent`
(default `default_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`](_autosummary/an.genres.html.md#an.genres.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 an `ExtensionAction` read before its genre
loaded is validated by the registered model on the way through.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`FlatAction`](_autosummary/an.ir.compose.html.md#an.ir.compose.FlatAction)]

### an.init(project_dir, , name=None, force=False)

Create a fresh an project at `project_dir`.

Idempotent unless the directory already contains a non-empty `scene.md`;
pass `force=True` to overwrite. Returns the absolute project root. The
project’s `.gitignore` gains `PROJECT_GITIGNORE` (lines it lacks).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.ir_to_markdown(scene)

Render a SceneIR back into the structured Markdown form.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> 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 existing `scene.md` goes through
`merge_markdown()`, which keeps the author’s text wherever the content
did not change.

### an.load(project_dir, , check_kinds=True)

Load an existing project. Reconciles scene.md / ir/scene.json first.

Registers the installed genres first ([`an.genres.load()`](_autosummary/an.genres.html.md#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’s
`play`, `expression` and `character` — 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()`](_autosummary/an.ir.validate.html.md#an.ir.validate.require_registered_kinds)): the
schema holds those as `str` (ADR 0001 decision 2), so without this a
typo’d `kind: enviroment` would load and render silently without its
backdrop. `check_kinds=False` is for `an validate`, which reports them
as findings instead.

* **Return type:**
  [`Project`](_autosummary/an.project.html.md#an.project.Project)

### an.loop(action, count)

Repeat `action` `count` times.

* **Return type:**
  [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction)

### an.markdown_to_ir(md_text)

Parse the structured Markdown form of a scene into a SceneIR.

* **Return type:**
  [`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR)

```pycon
>>> 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)

Run all children at once. Total duration = max of child durations.

* **Return type:**
  [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction)

### an.save(project)

Persist a Project’s current scene back to disk (md + json).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.sequence(\*actions)

Run children one after the other. Total duration = sum of child durations.

* **Return type:**
  [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction)

### an.set_(target, property, value, , at=0.0)

Discrete property set at time `at` (relative to its enclosing scope).

* **Return type:**
  [`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction)

### an.stagger(lag, \*actions)

Start each action `lag` seconds after the previous one STARTS.

The **stagger** (Manim’s `LaggedStart`, `previz`’s compose, a crowd
entering one by one): the children run in parallel, the `i`-th delayed
by `i * lag`. It is authoring sugar, not a new kind — it builds the
`parallel` of `sequence(delay(i * lag), action)` it means, so the
scene document, `scene.md` and every renderer see only core kinds.
Total duration: the latest child’s end. `scene.md` holds it verbatim (a
`kind: parallel` entry), so it round-trips. (`an.stage.text.reveal_units` —
`an.stage.text.stagger` before an#241 — is the text-block preset: a LIST of
per-unit actions with holds, not a combinator.)

* **Return type:**
  [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction)

```pycon
>>> 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)

Animate a property from `from_` (or its current value) to `to`.

`easing` left out inherits the scene’s `meta.default_easing` (else
`"ease_in_out"`); naming one — `"ease_in_out"` included — pins it.

* **Return type:**
  [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction)

```pycon
>>> "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)

Validate that `doc` (dict, JSON string, or SceneIR) conforms to the schema.

* **Return type:**
  [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)

```pycon
>>> 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)

Cross-field semantic checks. Pass live stores in for cross-store checks.

Both `available_voices` and `available_characters` accept any mapping;
`available_props` is the same thing for `kind="prop"` entities (an#108),
and `available_environments` lets 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 and `play` checks read descriptor
dicts, an#87 / an#7). Pass `None` to skip those checks — and know that
skipping them is what it sounds like: a `play` or a swap the compiler
will refuse passes silently without the store (the CLI, `an validate`,
always passes it).

`available_styles` is the project’s `styles` store: a check reads the
StylePack the scene names (`meta.style_pack`) from `ctx.stores["styles"]`
(a style’s `policy`, an#348).

`available_library_lock` is the project’s asset-library lockfile
(`mall["library_lock"]`): with it, every scene `library:` pin is checked
against the lockfile (`warning` on disagreement) and every pinned
check-out against its library version (`info` when it has been edited).

The checks are a REGISTRY ([`an.genres.registry.register_check()`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_check)):
the core’s own register below, a genre’s when it is loaded (the cut-out
genre’s `play`, `expression`, turn and view checks), and they run in
stages — `scene`, then `shot` once per shot, then `finish` — each by
its `order`. 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.

`only` runs just the registered checks of those names (what `an render`
does after synthesis, `post_synthesis_findings()`); `fps` is 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:**
  [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)

### Modules

| [`adapters`](_autosummary/an.adapters.html.md#module-an.adapters)           | Renderer adapters — facades over backends (the stage, Manim, Remotion, whiteboard).                                                                     |
|----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`assemble`](_autosummary/an.assemble.html.md#module-an.assemble)           | Film assembly: rendered shots → one film, with transitions and a sound layer.                                                                           |
| [`audio`](_autosummary/an.audio.html.md#module-an.audio)                 | Audio pipeline — TTS and lip-sync providers + orchestration.                                                                                            |
| [`base`](_autosummary/an.base.html.md#module-an.base)                   | Core types, constants, and re-exports for an.                                                                                                           |
| [`bench`](_autosummary/an.bench.html.md#module-an.bench)                 | `an bench` — render a fixed corpus, compute a metrics panel, write one ledger row.                                                                      |
| [`build`](_autosummary/an.build.html.md#module-an.build)                 | Incremental re-processing: content-addressed build stages (ADR 0004).                                                                                   |
| [`capabilities`](_autosummary/an.capabilities.html.md#module-an.capabilities)   | Capabilities: what an asset, an engine or the environment affords, and the one matcher.                                                                 |
| [`captions`](_autosummary/an.captions.html.md#module-an.captions)           | Captions from the word timings the audio pipeline already computes (an#175).                                                                            |
| [`conftest`](_autosummary/an.conftest.html.md#module-an.conftest)           | Collection rules for the package's own doctests.                                                                                                        |
| [`credits`](_autosummary/an.credits.html.md#module-an.credits)             | What a rendered video owes, and to whom.                                                                                                                |
| [`data`](_autosummary/an.data.html.md#module-an.data)                   | Bundled non-Python resources (cutout JS runtime, etc.).                                                                                                 |
| [`determinism`](_autosummary/an.determinism.html.md#module-an.determinism)     | The determinism perimeter: what must stay true for a render to be reproducible.                                                                         |
| [`engines`](_autosummary/an.engines.html.md#module-an.engines)             | Engines: seekable things the core drives frame by frame, and the renderer that drives them.                                                             |
| [`environments`](_autosummary/an.environments.html.md#module-an.environments)   | Moved to [`an.stage.environments`](_autosummary/an.stage.environments.html.md#module-an.stage.environments) (an#247); this path is a LIVE alias of it. |
| [`formats`](_autosummary/an.formats.html.md#module-an.formats)             | Numbers as text: a declared subset of d3-format, for counters (an#342).                                                                                 |
| [`frame_clock`](_autosummary/an.frame_clock.html.md#module-an.frame_clock)     | The frame clock: WHEN each output frame samples scene time.                                                                                             |
| [`genres`](_autosummary/an.genres.html.md#module-an.genres)               | Genres: what a kind of animation adds to the core, declared as one object.                                                                              |
| [`ir`](_autosummary/an.ir.html.md#module-an.ir)                       | Scene IR — the single source of truth for a scene.                                                                                                      |
| [`iterate`](_autosummary/an.iterate.html.md#module-an.iterate)             | Iterative edit loop — free-text instruction → IR patch via Claude → re-render.                                                                          |
| [`library`](_autosummary/an.library.html.md#module-an.library)             | The asset library: reusable assets that outlive their videos (ADR 0005).                                                                                |
| [`live_api`](_autosummary/an.live_api.html.md#module-an.live_api)           | The one switch that says "yes, this run may spend money".                                                                                               |
| [`mcp`](_autosummary/an.mcp.html.md#module-an.mcp)                     | The `an` MCP server: a curated, generated surface over the vocabulary and the capability registry.                                                      |
| [`measurements`](_autosummary/an.measurements.html.md#module-an.measurements)   | Measured durations: shots whose renderer, not their author, decides their length.                                                                       |
| [`media`](_autosummary/an.media.html.md#module-an.media)                 | Frames to deliverables, engine-independent: the frame stage's resolves and the sinks.                                                                   |
| [`motion`](_autosummary/an.motion.html.md#module-an.motion)               | Motion presets: a named vocabulary of moves, as authoring macros.                                                                                       |
| [`orchestrate`](_autosummary/an.orchestrate.html.md#module-an.orchestrate)     | Orchestrator: validate → audio → render → verify.                                                                                                       |
| [`paint`](_autosummary/an.paint.html.md#module-an.paint)                 | Paint: what fills a shape when one flat colour is not enough -- a gradient.                                                                             |
| [`paths`](_autosummary/an.paths.html.md#module-an.paths)                 | Moved to [`an.stage.paths`](_autosummary/an.stage.paths.html.md#module-an.stage.paths) (an#247); this path is a LIVE alias of it.               |
| [`preview`](_autosummary/an.preview.html.md#module-an.preview)             | Moved to [`an.stage.preview`](_autosummary/an.stage.preview.html.md#module-an.stage.preview) (an#247); this path is a LIVE alias of it.           |
| [`probe`](_autosummary/an.probe.html.md#module-an.probe)                 | `an probe`: a shot's frame at chosen instants, through the very path `render` draws it (an#347).                                                        |
| [`project`](_autosummary/an.project.html.md#module-an.project)             | Project init/load/save — the on-disk anatomy of an an project.                                                                                          |
| [`props`](_autosummary/an.props.html.md#module-an.props)                 | Moved to [`an.stage.props`](_autosummary/an.stage.props.html.md#module-an.stage.props) (an#247); this path is a LIVE alias of it.               |
| [`props_cli`](_autosummary/an.props_cli.html.md#module-an.props_cli)         | `an props …` — a prop's asset folder from the shell (an#340).                                                                                           |
| [`raster`](_autosummary/an.raster.html.md#module-an.raster)               | Moved to [`an.stage.raster`](_autosummary/an.stage.raster.html.md#module-an.stage.raster) (an#247); this path is a LIVE alias of it.             |
| [`render`](_autosummary/an.render.html.md#module-an.render)               | Project-level rendering: per-shot mp4 → final composited mp4 via ffmpeg concat.                                                                         |
| [`semantic`](_autosummary/an.semantic.html.md#module-an.semantic)           | The semantic layer: one versioned vocabulary registry, methods, aspects and the matcher.                                                                |
| [`sound_anchors`](_autosummary/an.sound_anchors.html.md#module-an.sound_anchors) | Sound cues timed by the picture (an#317): `at: {when: <node>, reaches: {y: px}}` and `until:`.                                                          |
| [`sound_fetch`](_autosummary/an.sound_fetch.html.md#module-an.sound_fetch)     | Add a sound from a URL, with its provenance and the cut taken (an#318).                                                                                 |
| [`sounds`](_autosummary/an.sounds.html.md#module-an.sounds)               | Sound assets: what the sound layer plays, where it came from, and a synthesizer.                                                                        |
| [`sounds_cli`](_autosummary/an.sounds_cli.html.md#module-an.sounds_cli)       | `an sounds …`: the project's sounds store from the shell (an#318).                                                                                      |
| [`stage`](_autosummary/an.stage.html.md#module-an.stage)                 | The 2D stage: the default engine, shipped with `an` but outside the core (an#247).                                                                      |
| [`stores`](_autosummary/an.stores.html.md#module-an.stores)               | Project mall: a dict of dol-backed `MutableMapping` stores.                                                                                             |
| [`styles`](_autosummary/an.styles.html.md#module-an.styles)               | StylePack: art direction as a document, and the first reader the styles store has had.                                                                  |
| [`text`](_autosummary/an.text.html.md#module-an.text)                   | Moved to [`an.stage.text`](_autosummary/an.stage.text.html.md#module-an.stage.text) (an#247); this path is a LIVE alias of it.                 |
| [`timing`](_autosummary/an.timing.html.md#module-an.timing)               | The timing kernel: what is on screen at time `t`, as a pure function.                                                                                   |
| [`tools`](_autosummary/an.tools.html.md#module-an.tools)                 | User-facing utility functions, plus the SSOT list for CLI dispatch.                                                                                     |
| [`util`](_autosummary/an.util.html.md#module-an.util)                   | Internal helpers: file I/O, hashing, time arithmetic, light path utilities.                                                                             |
| [`verify`](_autosummary/an.verify.html.md#module-an.verify)               | Verification protocol — same interface for human, lint, vision-LM, MoVer.                                                                               |


# _autosummary/an.ir.assets.html.md

# an.ir.assets

Where a third-party asset came from, and what its licence obliges.

`an` composes work it did not create — avatar art from a generator, stock images,
fonts, commissioned SVG — into a video its user ships. A licence defect is the
only failure in this package that reaches *backwards* through completed work: a
video shipped with an unattributed CC BY asset cannot be un-shipped, whereas
every rendering bug can be fixed forward.

Until now there was nowhere to record any of it. The character descriptor’s
`metadata` dict carried a comment saying it *could* hold a licence; nothing ever
put one there.

### Why the field names look borrowed

They are. The rights fields are spelled exactly as `illustration.ImageResult`
spells them — `license`, `license_url`, `attribution`, `source_page_url`,
`author`, `author_url`, `cacheable`, `provider`, `id`, `url` — because
`illustration` is the federation’s image-retrieval package and its results are
the most likely thing to become an `AssetSource`. Identical names mean the
adapter is a dict copy rather than a rename table, and a rename table is where a
field quietly stops being carried.

`tests/` pins this literally. That is the precedent `artful` already set for
shared vocabulary across packages that must not depend on each other.

Two fields are `an`’s own, because `ImageResult` has no equivalent:

- `sha256` — the digest of the bytes as they entered the project. A licence
  attached to a URL is a licence attached to whatever that URL serves *today*;
  attached to a digest, it stays attached to the thing that was actually used.
- `cost_usd` — what acquiring it cost, if anything. **\`None\` means unknown, never
  free.** That is the federation’s rule for costs and it matters here for the
  same reason it matters in a plan: a `0.0` that means “we did not check” reads
  as “this was free” to every consumer downstream.

### The long-term home

This is a local definition of something three packages need — `illustration`
retrieves third-party images, `an` composes them, `reelee` ships the result — and
that is the signature of a missing federation primitive rather than three missing
local fields. It is proposed upstream as `lacing.Artifact.rights` (lacing#34).
`Artifact` is frozen and holds real deployed data, so that change needs a
registered migration and a coordinated release; until it lands, this mirror is
what keeps `an` from shipping unattributed work in the meantime.

### Module Attributes

| [`PRIVATE_STUDY`](_autosummary/an.ir.assets.html.md#an.ir.assets.PRIVATE_STUDY)                  | The recognised code for material its owner has not licensed at all — frames or art carved out of a film, a show, a book — that a user may study privately but must not publish (an#211).   |
|---------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`PUBLIC_DOMAIN`](_autosummary/an.ir.assets.html.md#an.ir.assets.PUBLIC_DOMAIN)                  | The recognised code for the public domain — no rights to clear, nothing owed (an#211).                                                                                                     |
| [`LicenseClass`](_autosummary/an.ir.assets.html.md#an.ir.assets.LicenseClass)                   | What a licence means for shipping the video it ends up in.                                                                                                                                 |
| [`PROVIDER_TERMS_RESTRICTIONS`](_autosummary/an.ir.assets.html.md#an.ir.assets.PROVIDER_TERMS_RESTRICTIONS)    | the words a credits report prints beside it.                                                                                                                                               |
| [`ATTRIBUTION_REQUIRING_LICENSES`](_autosummary/an.ir.assets.html.md#an.ir.assets.ATTRIBUTION_REQUIRING_LICENSES) | Licence codes that oblige the *user of the output* to credit someone.                                                                                                                      |
| [`NONCOMMERCIAL_LICENSES`](_autosummary/an.ir.assets.html.md#an.ir.assets.NONCOMMERCIAL_LICENSES)         | Licence codes for NON-COMMERCIAL use only, each owing a credit too (an#373): Creative Commons' NC family.                                                                                  |
| [`NONCOMMERCIAL_RESTRICTION`](_autosummary/an.ir.assets.html.md#an.ir.assets.NONCOMMERCIAL_RESTRICTION)      | What a `noncommercial` licence restricts, printed wherever one is listed (a provider's own terms say it in their own words instead).                                                       |

### Functions

| [`provider_terms_restriction`](_autosummary/an.ir.assets.html.md#an.ir.assets.provider_terms_restriction)(source)   | The restriction a provider-terms licence carries beyond its class, if any.      |
|---------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`license_class`](_autosummary/an.ir.assets.html.md#an.ir.assets.license_class)(source)                | What this asset's licence means for shipping the video (an#211).                |
| [`license_restriction`](_autosummary/an.ir.assets.html.md#an.ir.assets.license_restriction)(source)          | What a licence restricts beyond its class's credit, for a report line (an#373). |
| [`normalise_license`](_autosummary/an.ir.assets.html.md#an.ir.assets.normalise_license)(code)              | A licence code folded to lowercase words joined by `-`.                         |
| [`requires_attribution`](_autosummary/an.ir.assets.html.md#an.ir.assets.requires_attribution)(source)         | Whether shipping this asset obliges the user to credit someone.                 |

### Classes

| [`AssetSource`](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource)(\*\*data)   | Provenance and rights for one third-party asset.   |
|--------------------------------------------------------------------------|----------------------------------------------------|

### an.ir.assets.ATTRIBUTION_REQUIRING_LICENSES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'by', 'by-sa', 'cc-by', 'cc-by-4.0', 'cc-by-nd', 'cc-by-sa', 'cc-by-sa-4.0'})*

Licence codes that oblige the *user of the output* to credit someone.

Matched case-insensitively against the licence code. Deliberately a small,
explicit set rather than a pattern: “does this oblige me” is a question with
a legal answer, and a regex that guesses is worse than a list that admits what
it does not know. An unrecognised licence is reported as UNKNOWN, which is
not the same as “no obligation”.

### *class* an.ir.assets.AssetSource(\*\*data)

Bases: `BaseModel`

Provenance and rights for one third-party asset.

```pycon
>>> s = AssetSource(provider="dicebear", id="lorelei/amy", license="cc0-1.0")
>>> requires_attribution(s)
False
>>> s = AssetSource(provider="dicebear", id="adventurer/amy", license="cc-by-4.0")
>>> requires_attribution(s)
True
```

#### cacheable *: [bool](https://docs.python.org/3/builtins/functions.html#bool) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`False` —
they do not (the Freesound API keeps every sound by reference), and no
store takes them; `None` — not recorded, which is not a yes (an#332,
as `lacing.Rights`); `True` — they do.

* **Type:**
  Whether the terms let the bytes be KEPT (stored, cached)

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.ir.assets.LicenseClass

What a licence means for shipping the video it ends up in.

- `attribution` — shippable, with a credit that MUST be displayed;
- `free` — shippable, nothing owed (public domain, CC0, MIT-shaped);
- `noncommercial` — shippable in a personal or private video, with its
  credit displayed, but NOT for commercial use (a monetised, sponsored or
  client video): CC BY-NC and its variants, the ElevenLabs free plan (an#373);
- `private` — NOT shippable: all rights reserved, private study only;
- `unknown` — not classified, which is not the same as free.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘attribution’, ‘free’, ‘noncommercial’, ‘private’, ‘unknown’]

### an.ir.assets.NONCOMMERCIAL_LICENSES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'by-nc', 'by-nc-nd', 'by-nc-sa', 'cc-by-nc', 'cc-by-nc-nd', 'cc-by-nc-sa'})*

Licence codes for NON-COMMERCIAL use only, each owing a credit too (an#373):
Creative Commons’ NC family. Matched like [`ATTRIBUTION_REQUIRING_LICENSES`](_autosummary/an.ir.assets.html.md#an.ir.assets.ATTRIBUTION_REQUIRING_LICENSES)
(a trailing version counts as its family: `cc-by-nc-sa-4.0`).

### an.ir.assets.NONCOMMERCIAL_RESTRICTION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'non-commercial use only (no monetised, sponsored or client video); credit owed'*

What a `noncommercial` licence restricts, printed wherever one is listed
(a provider’s own terms say it in their own words instead).

### an.ir.assets.PRIVATE_STUDY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'all-rights-reserved-private-study'*

The recognised code for material its owner has not licensed at all — frames
or art carved out of a film, a show, a book — that a user may study
privately but must not publish (an#211). Any code that normalises to one
starting with `all-rights-reserved` or `private-study` is this class,
so `"All rights reserved - private study only"` is recognised too.

### an.ir.assets.PROVIDER_TERMS_RESTRICTIONS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'elevenlabs-free-plan': 'ElevenLabs free plan: non-commercial use only, and the video must credit ElevenLabs (elevenlabs.io)', 'stability-community': "Stability AI Community License: the licence ends once you (with affiliates) make over USD 1,000,000 a year (then an Enterprise licence is needed), and use must follow Stability's acceptable use policy"}*

the words a
credits report prints beside it.

* **Type:**
  What a provider-terms code restricts beyond its class, by code

### an.ir.assets.PUBLIC_DOMAIN *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'public-domain'*

The recognised code for the public domain — no rights to clear, nothing
owed (an#211). `pd`, `pd-us`, `pdm-1.0`, `public-domain`,
`cc-pdm-1.0` and `cc0-*` are all this class.

### an.ir.assets.license_class(source)

What this asset’s licence means for shipping the video (an#211).

* **Return type:**
  [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'attribution'`, `'free'`, `'noncommercial'`, `'private'`, `'unknown'`]

```pycon
>>> license_class(AssetSource(provider="p", license="pd"))
'free'
>>> license_class(AssetSource(provider="p", license="all-rights-reserved"))
'private'
>>> license_class(AssetSource(provider="p", license="cc-by-4.0"))
'attribution'
>>> license_class(AssetSource(provider="freesound", license="cc-by-nc-4.0"))
'noncommercial'
>>> license_class(AssetSource(provider="p", license="bespoke"))
'unknown'
```

A provider’s terms count for what that provider made (`PROVIDER_TERMS`):

```pycon
>>> license_class(AssetSource(provider="elevenlabs", license="elevenlabs-paid-plan"))
'free'
>>> license_class(AssetSource(provider="openai", license="elevenlabs-paid-plan"))
'unknown'
```

### an.ir.assets.license_restriction(source)

What a licence restricts beyond its class’s credit, for a report line (an#373).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> license_restriction(AssetSource(provider="freesound", license="cc-by-nc-4.0"))[:24]
'non-commercial use only '
>>> license_restriction(AssetSource(provider="p", license="cc-by-4.0")) is None
True
```

### an.ir.assets.normalise_license(code)

A licence code folded to lowercase words joined by `-`.

Free text is what people actually write in a licence field, so the
classifier reads through punctuation and spacing:

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> normalise_license("All rights reserved - private study only; never publish")
'all-rights-reserved-private-study-only-never-publish'
>>> normalise_license(" CC-BY-4.0 ")
'cc-by-4-0'
```

### an.ir.assets.provider_terms_restriction(source)

The restriction a provider-terms licence carries beyond its class, if any.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> provider_terms_restriction(AssetSource(provider="elevenlabs", license="elevenlabs-free-plan"))[:21]
'ElevenLabs free plan:'
>>> provider_terms_restriction(AssetSource(provider="openai", license="elevenlabs-free-plan")) is None
True
```

### an.ir.assets.requires_attribution(source)

Whether shipping this asset obliges the user to credit someone.

Returns `None` for an unrecognised or absent licence: “we do not know” is a
distinct answer from “no”, and collapsing them is how an obligation gets
silently dropped. Private-study material (all rights reserved) also answers
`None` here — the question is not whom to credit but that it may not ship
at all; [`license_class()`](_autosummary/an.ir.assets.html.md#an.ir.assets.license_class) says so (`"private"`).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool) | [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.ir.camera.html.md

# an.ir.camera

Camera semantics: the named moves, and the one resolver that expands them.

This module exists because the alternative was two hand-maintained tables. The
compiler owned `CAMERA_MOVES` and `an.ir.validate` owned a frozenset of the
same names, reconciled by a test — which works, and is not what “a move that
validates cannot then raise” means. It means *one table*.

It lives under `an/ir/` rather than in the cutout adapter because the IR layer
must not import an adapter, and validate is in the IR layer. The camera is IR
semantics anyway: `Camera`/`CameraKey` are schema models, and a renderer that
cannot honour a move says so through `can_render`, not by keeping its own
vocabulary.

The geometry it encodes, verified numerically against the vendored PixiJS
composition: `root.pivot` IS a 2D camera, because PixiJS composes
`world = position + M·(local − pivot)` and the runtime indexes the centre
container as `root`. `+x` moves the CAMERA right, which moves content left.

### Module Attributes

| [`PAN_FRACTION`](_autosummary/an.ir.camera.html.md#an.ir.camera.PAN_FRACTION)   | How far a pan travels, as a fraction of the canvas width (a tilt uses the same fraction of the height).   |
|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| [`CAMERA_MOVES`](_autosummary/an.ir.camera.html.md#an.ir.camera.CAMERA_MOVES)   | The named moves, as KEY LISTS.                                                                            |

### Functions

| [`camera_keys`](_autosummary/an.ir.camera.html.md#an.ir.camera.camera_keys)(shot, \*, width, height)          | The shot's camera as an explicit key list — the ONE resolver.                            |
|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [`camera_shake_offsets`](_autosummary/an.ir.camera.html.md#an.ir.camera.camera_shake_offsets)(shot, \*, width, height) | The shot's camera shakes as `(time, dx, dy)` screen offsets — the ONE resolver (an#429). |

### Exceptions

| [`CameraError`](_autosummary/an.ir.camera.html.md#an.ir.camera.CameraError)   | A camera that cannot be resolved into keys.   |
|----------------------------------------------------------------|-----------------------------------------------|

### an.ir.camera.CAMERA_MOVES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[float](https://docs.python.org/3/builtins/functions.html#float)], [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[CameraKey](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraKey)]]]* *= {'hold': <function <lambda>>, 'pan_left': <function <lambda>>, 'pan_right': <function <lambda>>, 'pull_out': <function <lambda>>, 'push_in': <function <lambda>>, 'tilt_down': <function <lambda>>, 'tilt_up': <function <lambda>>, 'zoom_in': <function <lambda>>, 'zoom_out': <function <lambda>>}*

The named moves, as KEY LISTS. `move` is sugar over `keys` — one code path,
two front doors.

The five zoom moves must desugar to **exactly** the document they produced
before an#109, which is more specific than “a scale tween”: two animations
named `__camera__<shot>_scale_x`/`_scale_y`, one channel each targeting
`root`, keyframes `[(0.0, s0, "ease_in_out"), (duration, s1, null)]`, on two
tracks rooted at `"__camera__"`, and **no pivot channels**. The emitter emits
only the channels that actually vary, which is what makes that true rather
than merely intended — and what keeps every camera scene’s contract hash
where it was.

### *exception* an.ir.camera.CameraError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A camera that cannot be resolved into keys.

A plain `ValueError` subclass so the IR layer can raise it without knowing
about any renderer; the cutout compiler re-raises it as a
`CutoutCompileError` at its own boundary.

### an.ir.camera.PAN_FRACTION *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.3333333333333333*

How far a pan travels, as a fraction of the canvas width (a tilt uses the
same fraction of the height). A third of the frame is a legible move at any
resolution and is the one number a named pan has to choose; an author who
wants a different distance writes `keys` and says so.

### an.ir.camera.camera_keys(shot, , width, height)

The shot’s camera as an explicit key list — the ONE resolver.

`move` and `keys` are two front doors on one path. \*\*Validate calls this
and so does the compiler\*\*, which is what makes “a move that validates
cannot then raise” true by construction rather than by two tables agreeing
— the arrangement it replaced (an#109 review, H-1).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`CameraKey`](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraKey)]

```pycon
>>> from an.ir.schema import Camera, Shot
>>> push = Shot(id="s", renderer="cutout", duration=2.0, camera=Camera(move="push_in"))
>>> [(k.at, k.zoom) for k in camera_keys(push, width=320, height=240)]
[(0.0, 1.0), (2.0, 1.25)]
>>> pan = Shot(id="s", renderer="cutout", duration=2.0, camera=Camera(move="pan_left"))
>>> [(k.at, round(k.x, 3)) for k in camera_keys(pan, width=320, height=240)]
[(0.0, 0.0), (2.0, -106.667)]
```

### an.ir.camera.camera_shake_offsets(shot, , width, height)

The shot’s camera shakes as `(time, dx, dy)` screen offsets — the ONE resolver (an#429).

Empty when the shot has none. Each shake is at rest (`0, 0`) at its
start and its end; between, the frame jumps `frequency` times a second
to a seeded offset of at most `amplitude` × the frame height, scaled
down linearly to rest when `decay`. Validate and the compiler both call
this, so a shake that validates cannot then raise (as with
[`camera_keys()`](_autosummary/an.ir.camera.html.md#an.ir.camera.camera_keys)).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

```pycon
>>> from an.ir.schema import Camera, CameraShake, Shot
>>> s = Shot(id="s", duration=2.0, camera=Camera(shake=[CameraShake(at=1.0, duration=0.25, frequency=8)]))
>>> [(t, round(dx, 2), round(dy, 2)) for t, dx, dy in camera_shake_offsets(s, width=1280, height=720)]
[(1.0, 0.0, 0.0), (1.125, 3.72, 2.79), (1.25, 0.0, 0.0)]
```


# _autosummary/an.ir.compose.html.md

# an.ir.compose

Composition combinators for authoring action trees, plus a flattener.

The composition tree is the *authoring* form. The flat list of 

```
`
```

FlatAction\`s
with absolute start times is the *canonical* form that gets verified, cached,
and rendered. Both forms round-trip through the schema; the flat form is
what tooling reasons about.

```pycon
>>> from an.ir.compose import sequence, parallel, tween, delay, flatten
>>> action = sequence(
...     tween("charlie/torso", "rotation", to=10.0, duration=1.0),
...     delay(0.5),
...     tween("charlie/torso", "rotation", to=0.0, duration=1.0),
... )
>>> flat = flatten(action)
>>> [round(f.start, 3) for f in flat]
[0.0, 1.5]
>>> [round(f.end, 3) for f in flat]
[1.0, 2.5]
>>> action2 = parallel(
...     tween("a", "x", to=1.0, duration=2.0),
...     tween("b", "y", to=1.0, duration=3.0),
... )
>>> flat2 = flatten(action2)
>>> [(round(f.start, 2), round(f.end, 2)) for f in flat2]
[(0.0, 2.0), (0.0, 3.0)]
```

### Module Attributes

| [`INHERIT`](_autosummary/an.ir.compose.html.md#an.ir.compose.INHERIT)    | `tween(..., easing=INHERIT)` — the default — leaves the easing UNSET, so the scene's `meta.default_easing` applies, else `"ease_in_out"` (an#166).   |
|-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`PlayExtent`](_autosummary/an.ir.compose.html.md#an.ir.compose.PlayExtent) | `PlayAction -> seconds` a play WITHOUT an explicit `duration` occupies in a `sequence`.                                                              |

### Functions

| [`delay`](_autosummary/an.ir.compose.html.md#an.ir.compose.delay)(duration)                                  | An empty span that consumes time.                                                                                                                                                                                                                                                           |
|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`duration_of`](_autosummary/an.ir.compose.html.md#an.ir.compose.duration_of)(action, \*[, play_extent])           | Compute the total duration of an action tree without evaluating it.                                                                                                                                                                                                                         |
| [`flatten`](_autosummary/an.ir.compose.html.md#an.ir.compose.flatten)(action, \*[, start, play_extent])        | Walk a composition tree, emitting leaf actions with absolute times.                                                                                                                                                                                                                         |
| [`iter_actions`](_autosummary/an.ir.compose.html.md#an.ir.compose.iter_actions)(action)                             | `action` and every action under it, depth first (composites through their kind's `children` hook).                                                                                                                                                                                          |
| [`kind_of`](_autosummary/an.ir.compose.html.md#an.ir.compose.kind_of)(action)                                  | The registered [`ActionKind`](_autosummary/an.genres.html.md#an.genres.ActionKind) that governs `action`.                                                                                                                                                                      |
| [`loop`](_autosummary/an.ir.compose.html.md#an.ir.compose.loop)(action, count)                              | Repeat `action` `count` times.                                                                                                                                                                                                                                                              |
| [`map_leaves`](_autosummary/an.ir.compose.html.md#an.ir.compose.map_leaves)(action, fn)                           | `action` with every LEAF replaced by `fn(leaf)`, composites rebuilt around them (whatever field a composite keeps its children in: its kind's `children` hook says which values are children, the model's fields say where they live, so `loop.child` and a genre's composite are reached). |
| [`parallel`](_autosummary/an.ir.compose.html.md#an.ir.compose.parallel)(\*actions)                              | Run all children at once.                                                                                                                                                                                                                                                                   |
| [`resolve_action`](_autosummary/an.ir.compose.html.md#an.ir.compose.resolve_action)(action)                           | `action` as its registered model (an `ExtensionAction` left open by a document read before its genre loaded is validated now).                                                                                                                                                              |
| [`sequence`](_autosummary/an.ir.compose.html.md#an.ir.compose.sequence)(\*actions)                              | Run children one after the other.                                                                                                                                                                                                                                                           |
| [`set_`](_autosummary/an.ir.compose.html.md#an.ir.compose.set_)(target, property, value, \*[, at])          | Discrete property set at time `at` (relative to its enclosing scope).                                                                                                                                                                                                                       |
| [`stagger`](_autosummary/an.ir.compose.html.md#an.ir.compose.stagger)(lag, \*actions)                          | Start each action `lag` seconds after the previous one STARTS.                                                                                                                                                                                                                              |
| [`tween`](_autosummary/an.ir.compose.html.md#an.ir.compose.tween)(target, property, to, duration, \*[, ...]) | Animate a property from `from_` (or its current value) to `to`.                                                                                                                                                                                                                             |

### Classes

| [`FlatAction`](_autosummary/an.ir.compose.html.md#an.ir.compose.FlatAction)(start, end, action)   | A leaf action with its absolute start and end times.                    |
|-----------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`FlattenContext`](_autosummary/an.ir.compose.html.md#an.ir.compose.FlattenContext)(out[, extent])    | What a kind's `flatten` hook gets: where to put leaves, how to recurse. |

### *class* an.ir.compose.FlatAction(start, end, action)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A 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.ir.compose.FlattenContext(out, extent=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a kind’s `flatten` hook gets: where to put leaves, how to recurse.

`extent` is the caller’s extent resolver ([`PlayExtent`](_autosummary/an.ir.compose.html.md#an.ir.compose.PlayExtent)), passed on
to every leaf’s `duration` hook.

#### flatten(action, t)

Flatten `action` starting at `t`; return the new cursor.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### an.ir.compose.INHERIT *= INHERIT*

`tween(..., easing=INHERIT)` — the default — leaves the easing UNSET, so
the scene’s `meta.default_easing` applies, else `"ease_in_out"` (an#166).
A sentinel rather than `None` because `None` already means linear.

### an.ir.compose.PlayExtent

`PlayAction -> seconds` a play WITHOUT an explicit `duration` occupies in
a `sequence`. The default is `default_play_extent()`; the compiler and
`an validate` pass one bound to the entity’s descriptor. Generically, it is
the caller’s **extent resolver**: it is handed to every leaf kind’s
`duration` hook ([`an.genres.ActionKind`](_autosummary/an.genres.html.md#an.genres.ActionKind)), and the kinds that have an
open-ended length (the cut-out genre’s `play`) consult it.

alias of [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.ir.compose.delay(duration)

An empty span that consumes time. Useful inside `sequence`.

* **Return type:**
  [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction)

### an.ir.compose.duration_of(action, , play_extent=None)

Compute the total duration of an action tree without evaluating it.

`play_extent` resolves a duration-less `play` (see [`PlayExtent`](_autosummary/an.ir.compose.html.md#an.ir.compose.PlayExtent)).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> duration_of(tween("a", "x", to=1.0, duration=2.0))
2.0
>>> duration_of(sequence(delay(0.5), tween("a", "x", to=1.0, duration=1.5)))
2.0
>>> duration_of(parallel(delay(0.5), delay(2.5)))
2.5
>>> duration_of(loop(delay(0.25), 4))
1.0
>>> duration_of(set_("a", "x", 1.0))
0.0
```

### an.ir.compose.flatten(action, , start=0.0, play_extent=None)

Walk a composition tree, emitting leaf actions with absolute times.

A `play` without `duration` advances a `sequence` by `play_extent`
(default `default_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`](_autosummary/an.genres.html.md#an.genres.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 an `ExtensionAction` read before its genre
loaded is validated by the registered model on the way through.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`FlatAction`](_autosummary/an.ir.compose.html.md#an.ir.compose.FlatAction)]

### an.ir.compose.iter_actions(action)

`action` and every action under it, depth first (composites through
their kind’s `children` hook). Unregistered kinds are yielded, not raised:
a validator walks with this to REPORT them.

```pycon
>>> [a.kind for a in iter_actions(sequence(delay(1.0), loop(delay(0.5), 2)))]
['sequence', 'delay', 'loop', 'delay']
```

### an.ir.compose.kind_of(action)

The registered [`ActionKind`](_autosummary/an.genres.html.md#an.genres.ActionKind) that governs `action`.

Raises [`UnregisteredKindError`](_autosummary/an.genres.html.md#an.genres.UnregisteredKindError), naming the genre that
provides it, for a kind nobody registered.

* **Return type:**
  [`ActionKind`](_autosummary/an.genres.registry.html.md#an.genres.registry.ActionKind)

```pycon
>>> kind_of(delay(1.0)).name
'delay'
```

### an.ir.compose.loop(action, count)

Repeat `action` `count` times.

* **Return type:**
  [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction)

### an.ir.compose.map_leaves(action, fn)

`action` with every LEAF replaced by `fn(leaf)`, composites rebuilt
around them (whatever field a composite keeps its children in: its kind’s
`children` hook says which values are children, the model’s fields say
where they live, so `loop.child` and a genre’s composite are reached).

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> act = sequence(delay(1.0), loop(set_("a", "x", 1.0), 2))
>>> renamed = map_leaves(act, lambda a: a.model_copy(update={"target": "b"}) if a.kind == "set" else a)
>>> [a.target for a in iter_actions(renamed) if a.kind == "set"]
['b']
```

### an.ir.compose.parallel(\*actions)

Run all children at once. Total duration = max of child durations.

* **Return type:**
  [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction)

### an.ir.compose.resolve_action(action)

`action` as its registered model (an `ExtensionAction` left open
by a document read before its genre loaded is validated now).

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> resolve_action(delay(0.5)).duration
0.5
```

### an.ir.compose.sequence(\*actions)

Run children one after the other. Total duration = sum of child durations.

* **Return type:**
  [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction)

### an.ir.compose.set_(target, property, value, , at=0.0)

Discrete property set at time `at` (relative to its enclosing scope).

* **Return type:**
  [`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction)

### an.ir.compose.stagger(lag, \*actions)

Start each action `lag` seconds after the previous one STARTS.

The **stagger** (Manim’s `LaggedStart`, `previz`’s compose, a crowd
entering one by one): the children run in parallel, the `i`-th delayed
by `i * lag`. It is authoring sugar, not a new kind — it builds the
`parallel` of `sequence(delay(i * lag), action)` it means, so the
scene document, `scene.md` and every renderer see only core kinds.
Total duration: the latest child’s end. `scene.md` holds it verbatim (a
`kind: parallel` entry), so it round-trips. (`an.stage.text.reveal_units` —
`an.stage.text.stagger` before an#241 — is the text-block preset: a LIST of
per-unit actions with holds, not a combinator.)

* **Return type:**
  [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction)

```pycon
>>> 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.ir.compose.tween(target, property, to, duration, , from_=None, easing=INHERIT)

Animate a property from `from_` (or its current value) to `to`.

`easing` left out inherits the scene’s `meta.default_easing` (else
`"ease_in_out"`); naming one — `"ease_in_out"` included — pins it.

* **Return type:**
  [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction)

```pycon
>>> "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'
```


# _autosummary/an.ir.html.md

# an.ir

Scene IR — the single source of truth for a scene.

Three layers, per the architectural spec:

- **Narrative** (`scene.md`) — human Markdown with structured fenced blocks.
- **Scene Graph** (`ir/scene.json`) — Pydantic-validated JSON. The SSOT.
- **Render Code** — generated per-backend, disposable.

This subpackage owns layer 2 (the Pydantic schema), the Markdown↔JSON sync that
keeps layer 1 and layer 2 in lock-step, the validators (schema + semantic),
the version migration registry, and the composition combinators that flatten
authoring-time DSL to canonical-form actions.

### Functions

| [`set_`](_autosummary/an.ir.html.md#an.ir.set_)(target, property, value, \*[, at])          | Discrete property set at time `at` (relative to its enclosing scope).          |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`tween`](_autosummary/an.ir.html.md#an.ir.tween)(target, property, to, duration, \*[, ...]) | Animate a property from `from_` (or its current value) to `to`.                |
| [`sequence`](_autosummary/an.ir.html.md#an.ir.sequence)(\*actions)                              | Run children one after the other.                                              |
| [`parallel`](_autosummary/an.ir.html.md#an.ir.parallel)(\*actions)                              | Run all children at once.                                                      |
| [`stagger`](_autosummary/an.ir.html.md#an.ir.stagger)(lag, \*actions)                          | Start each action `lag` seconds after the previous one STARTS.                 |
| [`crowd`](_autosummary/an.ir.html.md#an.ir.crowd)(id, \*, ref, count[, kind, store, ...])    | `count` entities of one asset (`kind`/`store`/`ref`) placed in `area`.         |
| [`fan_out`](_autosummary/an.ir.html.md#an.ir.fan_out)(action, members, \*[, crowd_id])         | A copy of `action` per member, its targets moved from the crowd to the member. |
| [`delay`](_autosummary/an.ir.html.md#an.ir.delay)(duration)                                  | An empty span that consumes time.                                              |
| [`loop`](_autosummary/an.ir.html.md#an.ir.loop)(action, count)                              | Repeat `action` `count` times.                                                 |
| [`flatten`](_autosummary/an.ir.html.md#an.ir.flatten)(action, \*[, start, play_extent])        | Walk a composition tree, emitting leaf actions with absolute times.            |
| [`validate_schema`](_autosummary/an.ir.html.md#an.ir.validate_schema)(doc)                             | Validate that `doc` (dict, JSON string, or SceneIR) conforms to the schema.    |
| [`validate_semantic`](_autosummary/an.ir.html.md#an.ir.validate_semantic)(scene, \*[, ...])              | Cross-field semantic checks.                                                   |
| [`migrate`](_autosummary/an.ir.html.md#an.ir.migrate)(doc[, target_version, kind])             | Migrate a document to `target_version` (default: its kind's current).          |
| [`register_migration`](_autosummary/an.ir.html.md#an.ir.register_migration)(kind, from_version, ...)      | Decorator: register a migration for one kind in `MIGRATIONS`.                  |
| [`register_kind`](_autosummary/an.ir.html.md#an.ir.register_kind)(kind)                              | Register a document kind.                                                      |
| [`kind_of`](_autosummary/an.ir.html.md#an.ir.kind_of)(doc, \*[, kind])                         | Resolve a document's kind, by explicit name or from its `kind` tag.            |
| [`markdown_to_ir`](_autosummary/an.ir.html.md#an.ir.markdown_to_ir)(md_text)                          | Parse the structured Markdown form of a scene into a SceneIR.                  |
| [`ir_to_markdown`](_autosummary/an.ir.html.md#an.ir.ir_to_markdown)(scene)                            | Render a SceneIR back into the structured Markdown form.                       |
| [`sync`](_autosummary/an.ir.html.md#an.ir.sync)(project_dir)                                | Reconcile `scene.md` and `ir/scene.json` inside a project directory.           |

### Classes

| [`SceneIR`](_autosummary/an.ir.html.md#an.ir.SceneIR)(\*\*data)                                 | Top-level Scene IR document.                             |
|----------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| [`Meta`](_autosummary/an.ir.html.md#an.ir.Meta)(\*\*data)                                    | Scene metadata.                                          |
| [`AssetRef`](_autosummary/an.ir.html.md#an.ir.AssetRef)(\*\*data)                                | Reference to an entry in a project store.                |
| [`Shot`](_autosummary/an.ir.html.md#an.ir.Shot)(\*\*data)                                    | A single rendered unit.                                  |
| [`Dialogue`](_autosummary/an.ir.html.md#an.ir.Dialogue)(\*\*data)                                | One line of spoken dialogue.                             |
| [`Camera`](_autosummary/an.ir.html.md#an.ir.Camera)(\*\*data)                                  | Camera state for a shot: a named move, or explicit keys. |
| [`Resolution`](_autosummary/an.ir.html.md#an.ir.Resolution)(\*\*data)                              | Pixel dimensions of the rendered output.                 |
| [`FlatAction`](_autosummary/an.ir.html.md#an.ir.FlatAction)(start, end, action)                    | A leaf action with its absolute start and end times.     |
| [`ValidationReport`](_autosummary/an.ir.html.md#an.ir.ValidationReport)([passed, findings])              | Result of running one or more validators.                |
| [`ValidationFinding`](_autosummary/an.ir.html.md#an.ir.ValidationFinding)(severity, ir_path, description) | A single validation issue with a path into the IR.       |
| [`DocumentKind`](_autosummary/an.ir.html.md#an.ir.DocumentKind)(name, version_field, ...)            | A schema-versioned document kind.                        |

### *class* an.ir.AssetRef(\*\*data)

Bases: `_IRModel`

Reference 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. `overrides` lets a single shot tweak presentation without
forking the asset.

```pycon
>>> AssetRef(kind="character", id="maya", store="characters", ref="maya-v1").id
'maya'
```

#### kind *: [str](https://docs.python.org/3/builtins/stdtypes.html#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 `str` in the schema, not a `Literal` (ADR 0001 decision 2): the
values are the REGISTERED entity kinds ([`an.genres`](_autosummary/an.genres.html.md#module-an.genres)) — the core’s
`environment`, `prop` and `voice`, a genre’s `character` — and
`an validate` checks it against that registry, naming the genre that
provides an unregistered one.

* **Type:**
  `"style"` was retired in an#106

#### library *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#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()`](_autosummary/an.library.ids.html.md#an.library.ids.parse_ref), a pinned version required: `vNNN` or
`sha256:<prefix>`, never `latest`). `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, so
`ref` still 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 unset `library`
leaves no trace in a dump, so no stored scene changes and no schema
version moves.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### stage *: [StagePlacement](_autosummary/an.ir.schema.html.md#an.ir.schema.StagePlacement) | [None](https://docs.python.org/3/builtins/constants.html#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 `AssetRef` never reaches
it. So this field can grow without retiring a single ledger row.

### *class* an.ir.Camera(\*\*data)

Bases: `_CameraShakes`

Camera state for a shot: a named move, or explicit keys.

```pycon
>>> 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 named `move` desugars to a key list; `keys` is 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.

`keys` defaults to `None`, not `[]`, and that too is load-bearing: the
markdown writer dumps the camera with `exclude_none=True`, which KEEPS
empty lists — an empty default would write `keys: []` into every camera
block it regenerates.

`position`, `target` and `focal_length` were removed in an#109. They were
written into every `scene.md` this package ever generated and read by
nothing; a registered migration drops them.

#### keys *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[CameraKey](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraKey)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The explicit door. `None` = use `move`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### move *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

A named preset — sugar for `keys`. The cutout renderer’s vocabulary is
`an.stage.compile.CAMERA_MOVES`; validate and the compiler are
pinned to the same table by test, because a move that validates and then
raises is the failure `_check_renderable` exists to prevent.

### *class* an.ir.Dialogue(\*\*data)

Bases: `_IRModel`

One line of spoken dialogue.

`start` and `duration` are None until the audio pipeline runs (TTS
gives us a real duration); the pipeline stamps them then, deriving
`start` from the author’s `pause` / `at` ([`planned_start()`](_autosummary/an.ir.html.md#an.ir.Dialogue.planned_start)).

#### at *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Where this line starts, in SHOT seconds, whatever came before it —
`(at 3.0)` in `scene.md`. `start` is what the audio pipeline
DERIVES from `at`/`pause` on every pass; these two are what the
author wrote (an#187).

#### direction *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

How the line is DELIVERED — cues such as `["excited"]` or
`["sighs", "annoyed"]`, `{excited}` in `scene.md` (an#209). A TTS
model that takes inline audio tags (ElevenLabs v3/v4) receives them as
`[excited] Hi!`; others ignore them. Never part of `text`, so
captions and lip-sync alignment never see a cue.

#### emotion_intensity *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`maya [angry 0.4]: …`
in `scene.md`, a typed parameter on the (b-name) emotion. `None` is
full strength, and is omitted from JSON.

* **Type:**
  How strongly `emotion` shows, 0..1 (an#253)

#### leveled *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None)*

<the synthesized
line’s audio_ref>, “gain_db”: <its voice’s gain>}\`\` when `audio_ref`
is the leveled audio. `None` — unleveled — is omitted from JSON.

* **Type:**
  Stamped by voice leveling (an#315)
* **Type:**
  ```
  ``
  ```

  {“source”

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### pause *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Seconds of silence before this line, after the previous line’s speech
ends (the shot start, for the first line) — `(pause 1.5)` in `scene.md`.

#### planned_start(cursor)

Where this line starts, given the previous line ends at `cursor`.

The one rule the audio pipeline stamps into `start` and `an validate`
lays lines out by: `at` if set, else `cursor + pause`. A `start`
on a line never synthesized (no `audio_ref`) was authored — the
spelling of `at` before an#187 — and counts as one; a synthesized
line’s `start` is the pipeline’s own stamp, re-derived here.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> 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](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

seconds from the line’s start to the end of its
AUDIBLE speech — its take’s own trailing silence left out — stamped
with `duration` from the line’s audio. The next line’s `pause` counts
from here (a line with no pause still follows the whole take).
`None` (silent audio, a line its voice’s `trim_silence` already cut to
the tail the author keeps, a line stamped before an#397) counts from the
end of `duration`, 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), so `scene.json` and its fixtures do not change.

* **Type:**
  DERIVED (an#397)

#### word_timings *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[WordTimingIR](_autosummary/an.ir.schema.html.md#an.ir.schema.WordTimingIR)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The provider’s word timings, line-relative; `None` when the provider
has none (offline, Rhubarb) or the line was stamped before an#96.

### *class* an.ir.DocumentKind(name, version_field, current_version)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A schema-versioned document kind.

`version_field` differs between kinds (`version` for the scene IR,
`schema_version` for the character descriptor), which is exactly why a
migrator cannot simply reach for `doc["version"]`.

#### version_of(doc)

The document’s declared version, defaulting to this build’s.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### *class* an.ir.FlatAction(start, end, action)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A 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.ir.Meta(\*\*data)

Bases: `_IRModel`

Scene metadata.

#### captions *: [Captions](_autosummary/an.ir.schema.html.md#an.ir.schema.Captions) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Captions from the dialogue’s word timings (`Captions`, an#175);
`None` — the default — is none, omitted from JSON like `style_pack`.

#### closing_transition *: [Transition](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition) | [None](https://docs.python.org/3/builtins/constants.html#None)*

a `fade` to its `color` over its
`duration`, 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 a `cut`) 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](https://docs.python.org/3/builtins/stdtypes.html#str) | [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The easing every authored `tween` that 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 like `style_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, `play` clips and swap channels have none to
inherit. There is no per-shot override yet — style is a scene’s.

#### default_renderer *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

a registered renderer’s name.

* **Type:**
  Like [`Shot.renderer`](_autosummary/an.ir.html.md#an.ir.Shot.renderer)

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### sounds *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[SoundCue](_autosummary/an.ir.schema.html.md#an.ir.schema.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](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Stepped timing for AUTHORED TWEENS, in pose updates per second; `None`
(the default) leaves every tween smooth. At 30 fps, `15` is “on twos”
and `10` “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 and `play` clips: only `tween` curves 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 (a `set` at an off-grid `at`
likewise lands where it was authored). A shot’s own `step_hz`
overrides this. Must be positive (schema) and `<= fps` (validate +
compile), an#89.

#### style_pack *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The `StylePack` in the project’s `styles` store 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 by
`an 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](_autosummary/an.ir.schema.html.md#an.ir.schema.VoiceLoudness) | [None](https://docs.python.org/3/builtins/constants.html#None)*

One loudness for every voice (`VoiceLoudness`, an#315); `None`
— the default — levels nothing, and is omitted from JSON.

### *class* an.ir.Resolution(\*\*data)

Bases: `_IRModel`

Pixel dimensions of the rendered output.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.SceneIR(\*\*data)

Bases: `_IRModel`

Top-level Scene IR document. The SSOT.

A document is portable, diffable, and renderer-agnostic. Persisted as JSON
at `ir/scene.json` inside an an project.

```pycon
>>> 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'
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.Shot(\*\*data)

Bases: `_IRModel`

A single rendered unit. A scene is a sequence of shots.

A shot’s `renderer` selects the backend that draws it. Every renderer must accept the
same Shot fields; renderer-specific options go under `options`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### policy *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/builtins/stdtypes.html#str)*

Which RENDERER draws this shot — not art direction. The field was
called `style` until an#106, colliding with the styles store (which
holds art direction) and with `AssetRef(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.
A `str` in the schema (ADR 0001 decision 2): any name a renderer
registered (`an.adapters.register_renderer`), checked by `an
validate`. `cutout` stays the persisted default (decision 9).

#### sounds *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[SoundCue](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue)]*

Sound cues in SHOT-local time (`SoundCue`).

#### step_hz *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per-shot override of [`Meta.step_hz`](_autosummary/an.ir.html.md#an.ir.Meta.step_hz) (`None` = inherit).

#### transition *: [Transition](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition) | [None](https://docs.python.org/3/builtins/constants.html#None)*

How this shot is entered (`Transition`); `None` is a hard cut.

### *class* an.ir.ValidationFinding(severity, ir_path, description, location=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single validation issue with a path into the IR.

#### location *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`"<file>:<line>"` when the thing to fix is an opaque source a shot runs
(a Manim scene file, an#279) rather than the IR; `None` otherwise.

### *class* an.ir.ValidationReport(passed=True, findings=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of running one or more validators.

`passed` is True iff there are no error-severity findings.

### an.ir.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)

`count` entities of one asset (`kind`/`store`/`ref`) placed in `area`.

id: the crowd’s name; member `k` is `<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 as `character`)
store: the store the asset lives in (default: the kind’s registered store)
layout: `grid` (rows filled from the back), `row` or `scatter`
area: `(x0, y0, x1, y1)` in scene pixels about the stage centre; a

> member’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

```pycon
>>> [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
```

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`AssetRef`](_autosummary/an.ir.schema.html.md#an.ir.schema.AssetRef)]

### an.ir.delay(duration)

An empty span that consumes time. Useful inside `sequence`.

* **Return type:**
  [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction)

### an.ir.fan_out(action, members, , crowd_id=None)

A copy of `action` per member, its targets moved from the crowd to the member.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

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 (`AssetRef` s 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()`](_autosummary/an.ir.compose.html.md#an.ir.compose.stagger) for a ripple through the ranks,
or [`parallel()`](_autosummary/an.ir.compose.html.md#an.ir.compose.parallel) for the crowd moving as one.

```pycon
>>> 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.ir.flatten(action, , start=0.0, play_extent=None)

Walk a composition tree, emitting leaf actions with absolute times.

A `play` without `duration` advances a `sequence` by `play_extent`
(default `default_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`](_autosummary/an.genres.html.md#an.genres.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 an `ExtensionAction` read before its genre
loaded is validated by the registered model on the way through.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`FlatAction`](_autosummary/an.ir.compose.html.md#an.ir.compose.FlatAction)]

### an.ir.ir_to_markdown(scene)

Render a SceneIR back into the structured Markdown form.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> 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 existing `scene.md` goes through
`merge_markdown()`, which keeps the author’s text wherever the content
did not change.

### an.ir.kind_of(doc, , kind=None)

Resolve a document’s kind, by explicit name or from its `kind` tag.

Raises `ValueError` for an unregistered kind rather than guessing — a
document whose kind nobody declared has no known version field, so any
answer would be a fabrication.

* **Return type:**
  [`DocumentKind`](_autosummary/an.ir.html.md#an.ir.DocumentKind)

### an.ir.loop(action, count)

Repeat `action` `count` times.

* **Return type:**
  [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction)

### an.ir.markdown_to_ir(md_text)

Parse the structured Markdown form of a scene into a SceneIR.

* **Return type:**
  [`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR)

```pycon
>>> 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.ir.migrate(doc, target_version=None, , kind=None)

Migrate a document to `target_version` (default: its kind’s current).

Walks the migration registry one step at a time, considering only the
migrations registered for this document’s kind. Raises `ValueError` if no
path exists between the source and target versions.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> from an.base import SCHEMA_VERSION
>>> migrate({"version": SCHEMA_VERSION, "kind": "SceneIR"})["version"] == SCHEMA_VERSION
True
>>> migrate({"version": "0.1.0", "kind": "SceneIR"})["version"] == SCHEMA_VERSION
True
```

### an.ir.parallel(\*actions)

Run all children at once. Total duration = max of child durations.

* **Return type:**
  [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction)

### an.ir.register_kind(kind)

Register a document kind. Returns it, so callers can bind the result.

* **Return type:**
  [`DocumentKind`](_autosummary/an.ir.html.md#an.ir.DocumentKind)

### an.ir.register_migration(kind, from_version, to_version)

Decorator: register a migration for one kind in `MIGRATIONS`.

Registered against the throwaway `Widget` kind from the module docstring,
deliberately: this registry is process-wide, so a doctest that registered a
step for a REAL kind would leave a second path through the ladder for every
test that ran afterwards — which is exactly what happened once, and it
presented as one unrelated test failing only in a full run (an#106).

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]], [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]]

```pycon
>>> @register_migration("Widget", "1.0", "2.0")
... def _bump(doc):
...     doc["widget_version"] = "2.0"
...     return doc
>>> ("Widget", "1.0", "2.0") in MIGRATIONS
True
```

### an.ir.sequence(\*actions)

Run children one after the other. Total duration = sum of child durations.

* **Return type:**
  [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction)

### an.ir.set_(target, property, value, , at=0.0)

Discrete property set at time `at` (relative to its enclosing scope).

* **Return type:**
  [`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction)

### an.ir.stagger(lag, \*actions)

Start each action `lag` seconds after the previous one STARTS.

The **stagger** (Manim’s `LaggedStart`, `previz`’s compose, a crowd
entering one by one): the children run in parallel, the `i`-th delayed
by `i * lag`. It is authoring sugar, not a new kind — it builds the
`parallel` of `sequence(delay(i * lag), action)` it means, so the
scene document, `scene.md` and every renderer see only core kinds.
Total duration: the latest child’s end. `scene.md` holds it verbatim (a
`kind: parallel` entry), so it round-trips. (`an.stage.text.reveal_units` —
`an.stage.text.stagger` before an#241 — is the text-block preset: a LIST of
per-unit actions with holds, not a combinator.)

* **Return type:**
  [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction)

```pycon
>>> 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.ir.sync(project_dir)

Reconcile `scene.md` and `ir/scene.json` inside a project directory.

Strategy in v0.1: Markdown is the human SSOT; if both exist, the JSON is
regenerated from the Markdown unless mtimes show JSON is newer (which the
user is told never to do — but we warn instead of silently overwriting).

* **Return type:**
  `SyncResult`

### an.ir.tween(target, property, to, duration, , from_=None, easing=INHERIT)

Animate a property from `from_` (or its current value) to `to`.

`easing` left out inherits the scene’s `meta.default_easing` (else
`"ease_in_out"`); naming one — `"ease_in_out"` included — pins it.

* **Return type:**
  [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction)

```pycon
>>> "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.ir.validate_schema(doc)

Validate that `doc` (dict, JSON string, or SceneIR) conforms to the schema.

* **Return type:**
  [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)

```pycon
>>> 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.ir.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)

Cross-field semantic checks. Pass live stores in for cross-store checks.

Both `available_voices` and `available_characters` accept any mapping;
`available_props` is the same thing for `kind="prop"` entities (an#108),
and `available_environments` lets 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 and `play` checks read descriptor
dicts, an#87 / an#7). Pass `None` to skip those checks — and know that
skipping them is what it sounds like: a `play` or a swap the compiler
will refuse passes silently without the store (the CLI, `an validate`,
always passes it).

`available_styles` is the project’s `styles` store: a check reads the
StylePack the scene names (`meta.style_pack`) from `ctx.stores["styles"]`
(a style’s `policy`, an#348).

`available_library_lock` is the project’s asset-library lockfile
(`mall["library_lock"]`): with it, every scene `library:` pin is checked
against the lockfile (`warning` on disagreement) and every pinned
check-out against its library version (`info` when it has been edited).

The checks are a REGISTRY ([`an.genres.registry.register_check()`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_check)):
the core’s own register below, a genre’s when it is loaded (the cut-out
genre’s `play`, `expression`, turn and view checks), and they run in
stages — `scene`, then `shot` once per shot, then `finish` — each by
its `order`. 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.

`only` runs just the registered checks of those names (what `an render`
does after synthesis, `post_synthesis_findings()`); `fps` is 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:**
  [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)

### Modules

| [`assets`](_autosummary/an.ir.assets.html.md#module-an.ir.assets)     | Where a third-party asset came from, and what its licence obliges.         |
|---------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`camera`](_autosummary/an.ir.camera.html.md#module-an.ir.camera)     | Camera semantics: the named moves, and the one resolver that expands them. |
| [`compose`](_autosummary/an.ir.compose.html.md#module-an.ir.compose)   | Composition combinators for authoring action trees, plus a flattener.      |
| [`schema`](_autosummary/an.ir.schema.html.md#module-an.ir.schema)     | Pydantic v2 models for the Scene IR.                                       |
| [`validate`](_autosummary/an.ir.validate.html.md#module-an.ir.validate) | Schema and semantic validation for SceneIR documents.                      |


# _autosummary/an.ir.schema.html.md

# 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 `version` and
  `compatible_version`. Migrations live in `an.ir.migrate`.
- **Forward-compatible reads.** Top-level model has `extra="allow"` so a future
  field doesn’t crash an older reader.
- \*\*Open `Action` union\*\* (ADR 0001 decision 2). The union holds the core
  kinds (`set`, `tween`, `sequence`, `parallel`, `delay`, `loop`)
  and ONE open member, [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction), which a callable
  discriminator selects for any other `kind`. A genre registers its kinds
  ([`an.genres`](_autosummary/an.genres.html.md#module-an.genres)); a document’s `kind: play` then validates to the
  registered model, and before registration it stays an `ExtensionAction`
  that 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:

```pycon
>>> 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

| [`DFLT_SHAKE_DURATION`](_autosummary/an.ir.schema.html.md#an.ir.schema.DFLT_SHAKE_DURATION)       | 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.   |
|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`CORE_ACTION_KINDS`](_autosummary/an.ir.schema.html.md#an.ir.schema.CORE_ACTION_KINDS)         | The `kind` of every action the core defines (the static union members).                                                                                                                        |
| [`EXTENSION_TAG`](_autosummary/an.ir.schema.html.md#an.ir.schema.EXTENSION_TAG)             | The union tag of the open member.                                                                                                                                                              |
| [`Action`](_autosummary/an.ir.schema.html.md#an.ir.schema.Action)                    | the core kinds plus [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction) for any other `kind` (a registered genre kind validates to its own model through it).                     |
| [`DEFAULT_CAPTION_MAX_CHARS`](_autosummary/an.ir.schema.html.md#an.ir.schema.DEFAULT_CAPTION_MAX_CHARS) | the broadcast convention (BBC / Netflix timed-text guidance: 42 characters, two lines).                                                                                                        |
| [`DEFAULT_CAPTION_SIZE`](_autosummary/an.ir.schema.html.md#an.ir.schema.DEFAULT_CAPTION_SIZE)      | Caption type size as a fraction of frame height — a little under the title default, as captions are read while something else is watched.                                                      |
| [`STAGE_NODE_SPACE`](_autosummary/an.ir.schema.html.md#an.ir.schema.STAGE_NODE_SPACE)          | The property space a 2D stage engine's node lives in ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)).                                              |
| [`SPECIMEN_DURATION`](_autosummary/an.ir.schema.html.md#an.ir.schema.SPECIMEN_DURATION)         | How long a specimen shot runs (`an library sheet` draws its first frame).                                                                                                                      |
| [`SPECIMEN_SHOT_ID`](_autosummary/an.ir.schema.html.md#an.ir.schema.SPECIMEN_SHOT_ID)          | The id of a specimen shot.                                                                                                                                                                     |

### Functions

| [`resolve_step_hz`](_autosummary/an.ir.schema.html.md#an.ir.schema.resolve_step_hz)(shot, scene_step_hz)        | The stepped-timing policy `shot` renders under: its own `step_hz` when it declares one, else the scene's, else `None` (smooth).   |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| [`stage_specimen`](_autosummary/an.ir.schema.html.md#an.ir.schema.stage_specimen)(ref)                         | A short shot showing the one stage entity `ref` casts, on its own (an#347).                                                       |
| [`unregistered_action_kind`](_autosummary/an.ir.schema.html.md#an.ir.schema.unregistered_action_kind)(kind, \*[, where]) | The error for an action `kind` no loaded genre registered.                                                                        |

### Classes

| [`AssetRef`](_autosummary/an.ir.schema.html.md#an.ir.schema.AssetRef)(\*\*data)        | Reference to an entry in a project store.                                                                                                                           |
|----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`Camera`](_autosummary/an.ir.schema.html.md#an.ir.schema.Camera)(\*\*data)          | Camera state for a shot: a named move, or explicit keys.                                                                                                            |
| [`CameraFollow`](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraFollow)(\*\*data)    | A camera that follows an entity (an#445): the frame keeps `target` where it stood at the shot's start.                                                              |
| [`CameraKey`](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraKey)(\*\*data)       | One camera pose at one time — the explicit door behind the named moves.                                                                                             |
| [`CameraShake`](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraShake)(\*\*data)     | A jolt of the frame inside a shot (an#429): seeded screen-space jitter.                                                                                             |
| [`Captions`](_autosummary/an.ir.schema.html.md#an.ir.schema.Captions)(\*\*data)        | Captions for the whole film, built from the dialogue's word timings.                                                                                                |
| [`CueAnchor`](_autosummary/an.ir.schema.html.md#an.ir.schema.CueAnchor)(\*\*data)       | A sound cue's time, taken from the picture (an#317): when a node reaches a frame row or column.                                                                     |
| [`CueUntil`](_autosummary/an.ir.schema.html.md#an.ir.schema.CueUntil)(\*\*data)        | Where a sound cue ends: at another cue's start, plus `offset` (an#317).                                                                                             |
| [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction)(\*\*data)     | Composition: an empty span that consumes time.                                                                                                                      |
| [`Dialogue`](_autosummary/an.ir.schema.html.md#an.ir.schema.Dialogue)(\*\*data)        | One line of spoken dialogue.                                                                                                                                        |
| [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)(\*\*data) | An action of a kind the core does not define: the IR's one open member.                                                                                             |
| [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction)(\*\*data)      | Composition: repeat `child` `count` times.                                                                                                                          |
| [`Meta`](_autosummary/an.ir.schema.html.md#an.ir.schema.Meta)(\*\*data)            | Scene metadata.                                                                                                                                                     |
| [`Narration`](_autosummary/an.ir.schema.html.md#an.ir.schema.Narration)(\*\*data)       | Off-screen narration.                                                                                                                                               |
| [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction)(\*\*data)  | Composition: run all children simultaneously starting at the same time.                                                                                             |
| [`Resolution`](_autosummary/an.ir.schema.html.md#an.ir.schema.Resolution)(\*\*data)      | Pixel dimensions of the rendered output.                                                                                                                            |
| [`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR)(\*\*data)         | Top-level Scene IR document.                                                                                                                                        |
| [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction)(\*\*data)  | Composition: run children one after the other.                                                                                                                      |
| [`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction)(\*\*data)       | Set a property to a value at a specific time.                                                                                                                       |
| [`Shot`](_autosummary/an.ir.schema.html.md#an.ir.schema.Shot)(\*\*data)            | A single rendered unit.                                                                                                                                             |
| [`SoundCue`](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue)(\*\*data)        | One sound placed on the timeline: an SFX hit, an ambience, a music bed.                                                                                             |
| [`StagePlacement`](_autosummary/an.ir.schema.html.md#an.ir.schema.StagePlacement)(\*\*data)  | Where an entity stands on the stage, and how big it is.                                                                                                             |
| [`Transition`](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition)(\*\*data)      | How a shot is ENTERED — from the previous shot, or (for the first shot) from nothing.                                                                               |
| [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction)(\*\*data)     | Animate a property from a start value to an end value over a duration.                                                                                              |
| [`VisemeKeyframe`](_autosummary/an.ir.schema.html.md#an.ir.schema.VisemeKeyframe)(\*\*data)  | A single mouth-shape keyframe in a viseme track.                                                                                                                    |
| [`VisemeTrack`](_autosummary/an.ir.schema.html.md#an.ir.schema.VisemeTrack)(\*\*data)     | Aligned viseme track produced by the lip-sync stage.                                                                                                                |
| [`VoiceLoudness`](_autosummary/an.ir.schema.html.md#an.ir.schema.VoiceLoudness)(\*\*data)   | One loudness for every voice of the film (an#315).                                                                                                                  |
| [`WordTimingIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.WordTimingIR)(\*\*data)    | One word of a line and when it was spoken, in seconds from the line's start (like [`VisemeKeyframe`](_autosummary/an.ir.schema.html.md#an.ir.schema.VisemeKeyframe), never absolute). |

### an.ir.schema.Action

the core kinds plus [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction) for any other
`kind` (a registered genre kind validates to its own model through it).
`SerializeAsAny` makes a typed genre instance (a `PlayAction`) serialize
with its own fields rather than the open member’s.

* **Type:**
  Every action

alias of `Annotated`[`Annotated`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), `Tag`(tag=set)] | `Annotated`[[`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), `Tag`(tag=tween)] | `Annotated`[[`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), `Tag`(tag=sequence)] | `Annotated`[[`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), `Tag`(tag=parallel)] | `Annotated`[[`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), `Tag`(tag=delay)] | `Annotated`[[`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), `Tag`(tag=loop)] | `Annotated`[[`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction), `SerializeAsAny`(), `Tag`(tag=extension)], `Discriminator`(discriminator=`_action_tag`, custom_error_type=[`None`](https://docs.python.org/3/builtins/constants.html#None), custom_error_message=[`None`](https://docs.python.org/3/builtins/constants.html#None), custom_error_context=[`None`](https://docs.python.org/3/builtins/constants.html#None))]

### *class* an.ir.schema.AssetRef(\*\*data)

Bases: `_IRModel`

Reference 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. `overrides` lets a single shot tweak presentation without
forking the asset.

```pycon
>>> AssetRef(kind="character", id="maya", store="characters", ref="maya-v1").id
'maya'
```

#### kind *: [str](https://docs.python.org/3/builtins/stdtypes.html#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 `str` in the schema, not a `Literal` (ADR 0001 decision 2): the
values are the REGISTERED entity kinds ([`an.genres`](_autosummary/an.genres.html.md#module-an.genres)) — the core’s
`environment`, `prop` and `voice`, a genre’s `character` — and
`an validate` checks it against that registry, naming the genre that
provides an unregistered one.

* **Type:**
  `"style"` was retired in an#106

#### library *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#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()`](_autosummary/an.library.ids.html.md#an.library.ids.parse_ref), a pinned version required: `vNNN` or
`sha256:<prefix>`, never `latest`). `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, so
`ref` still 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 unset `library`
leaves no trace in a dump, so no stored scene changes and no schema
version moves.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### stage *: [StagePlacement](_autosummary/an.ir.schema.html.md#an.ir.schema.StagePlacement) | [None](https://docs.python.org/3/builtins/constants.html#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 `AssetRef` never reaches
it. So this field can grow without retiring a single ledger row.

### an.ir.schema.CORE_ACTION_KINDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('set', 'tween', 'sequence', 'parallel', 'delay', 'loop')*

The `kind` of every action the core defines (the static union members).

### *class* an.ir.schema.Camera(\*\*data)

Bases: `_CameraShakes`

Camera state for a shot: a named move, or explicit keys.

```pycon
>>> 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 named `move` desugars to a key list; `keys` is 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.

`keys` defaults to `None`, not `[]`, and that too is load-bearing: the
markdown writer dumps the camera with `exclude_none=True`, which KEEPS
empty lists — an empty default would write `keys: []` into every camera
block it regenerates.

`position`, `target` and `focal_length` were removed in an#109. They were
written into every `scene.md` this package ever generated and read by
nothing; a registered migration drops them.

#### keys *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[CameraKey](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraKey)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The explicit door. `None` = use `move`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### move *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

A named preset — sugar for `keys`. The cutout renderer’s vocabulary is
`an.stage.compile.CAMERA_MOVES`; validate and the compiler are
pinned to the same table by test, because a move that validates and then
raises is the failure `_check_renderable` exists to prevent.

### *class* an.ir.schema.CameraFollow(\*\*data)

Bases: `_IRModel`

A camera that follows an entity (an#445): the frame keeps `target`
where it stood at the shot’s start.

```pycon
>>> CameraFollow(target="bob").axes
'x'
```

`target` is an entity id or one of its nodes (`bob/head`); `axes` is
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.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.CameraKey(\*\*data)

Bases: `_IRModel`

One camera pose at one time — the explicit door behind the named moves.

```pycon
>>> CameraKey(at=1.0, x=-160.0, zoom=1.25).x
-160.0
```

**Sign convention**, stated because every surveyed tool disagrees: `+x`
moves the CAMERA right, which moves the content left. `zoom` is on-screen
magnification, so `1.25` means “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. `rotation` is camera
roll in radians.

`easing` defaults to **\`None\`, not \`”ease_in_out”\`**, and that is not a
style choice: today’s emitter puts `"ease_in_out"` on the first keyframe
and `null` on 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.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### rotation *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Camera roll, radians.

#### x *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Camera position in scene pixels. `+x` moves the camera right.

#### zoom *: [float](https://docs.python.org/3/builtins/functions.html#float)*

On-screen magnification. Must be > 0 — a zero or negative zoom is not a
camera, and the compiler would emit a degenerate root scale.

### *class* an.ir.schema.CameraShake(\*\*data)

Bases: `_IRModel`

A jolt of the frame inside a shot (an#429): seeded screen-space jitter.

```pycon
>>> 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, with `decay`, falls linearly to rest by
the end; the frame is exactly at rest before `at` and after it.

#### amplitude *: [float](https://docs.python.org/3/builtins/functions.html#float)*

How far the frame jumps at most, as a fraction of the frame height.

#### at *: [float](https://docs.python.org/3/builtins/functions.html#float)*

When the jolt starts, in shot seconds.

#### decay *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Fall linearly to rest over the duration (an impact); off, it holds its
amplitude to the end (a rumble).

#### duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

How long it lasts.

#### frequency *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Jumps per second.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### seed *: [int](https://docs.python.org/3/builtins/functions.html#int)*

the same seed draws the same jolt on every machine.

* **Type:**
  Which jitter

### *class* an.ir.schema.Captions(\*\*data)

Bases: `_IRModel`

Captions for the whole film, built from the dialogue’s word timings.

Present = on; `meta.captions` unset (the default) is no captions and no
trace in any document. One cue list ([`an.captions.caption_pages()`](_autosummary/an.captions.html.md#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 at `anchor` in the title-safe area), shown for exactly the
  frames the sidecar says;
- `sidecar`: a SubRip `.srt` written through the `captions` store,
  next to the delivered mp4, in FILM time (a dissolve shortens the film,
  and every later cue moves with it).

```pycon
>>> Captions().max_chars, Captions().anchor
(42, 'bottom')
>>> Captions(highlight="#ffcc00").highlight
'#ffcc00'
```

#### anchor *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

One of tituli’s nine title-safe anchors.

#### font *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`None` = the embedded face; else an ABSOLUTE font file path, or one
relative to the project directory.

#### highlight *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

the word being spoken is drawn in this colour (karaoke);
`None` draws every word in `color`.

* **Type:**
  `#rrggbb`

#### max_chars *: [int](https://docs.python.org/3/builtins/functions.html#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.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### strict *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

A line with no word timings is captioned with its words spread evenly
over its duration, with a warning; `strict` makes that an error.

### *class* an.ir.schema.CueAnchor(\*\*data)

Bases: `_IRModel`

A sound cue’s time, taken from the picture (an#317): when a node reaches a frame row or column.

`when` is a node path in a stage shot (`crawl/line_7`: a text block’s
unit, a prop, a plane); `reaches` is `{"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. `offset` is
added after. `shot` names the shot to look in, for a cue in
`meta.sounds` (default: the first shot that has the node).

```pycon
>>> CueAnchor(when="crawl/line_7", reaches={"y": 840}).axis
('y', 840.0)
```

#### *property* axis *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [float](https://docs.python.org/3/builtins/functions.html#float)]*

`(axis, pixels)`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.CueUntil(\*\*data)

Bases: `_IRModel`

Where a sound cue ends: at another cue’s start, plus `offset` (an#317).

`cue` is the `sound` key of a cue in the same list (the shot’s, or
`meta.sounds`); the first such cue is meant.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.ir.schema.DEFAULT_CAPTION_MAX_CHARS *: [int](https://docs.python.org/3/builtins/functions.html#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](https://docs.python.org/3/builtins/functions.html#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](https://docs.python.org/3/builtins/functions.html#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)

Bases: `_ActionBase`

Composition: an empty span that consumes time.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.Dialogue(\*\*data)

Bases: `_IRModel`

One line of spoken dialogue.

`start` and `duration` are None until the audio pipeline runs (TTS
gives us a real duration); the pipeline stamps them then, deriving
`start` from the author’s `pause` / `at` ([`planned_start()`](_autosummary/an.ir.schema.html.md#an.ir.schema.Dialogue.planned_start)).

#### at *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Where this line starts, in SHOT seconds, whatever came before it —
`(at 3.0)` in `scene.md`. `start` is what the audio pipeline
DERIVES from `at`/`pause` on every pass; these two are what the
author wrote (an#187).

#### direction *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

How the line is DELIVERED — cues such as `["excited"]` or
`["sighs", "annoyed"]`, `{excited}` in `scene.md` (an#209). A TTS
model that takes inline audio tags (ElevenLabs v3/v4) receives them as
`[excited] Hi!`; others ignore them. Never part of `text`, so
captions and lip-sync alignment never see a cue.

#### emotion_intensity *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`maya [angry 0.4]: …`
in `scene.md`, a typed parameter on the (b-name) emotion. `None` is
full strength, and is omitted from JSON.

* **Type:**
  How strongly `emotion` shows, 0..1 (an#253)

#### leveled *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [None](https://docs.python.org/3/builtins/constants.html#None)*

<the synthesized
line’s audio_ref>, “gain_db”: <its voice’s gain>}\`\` when `audio_ref`
is the leveled audio. `None` — unleveled — is omitted from JSON.

* **Type:**
  Stamped by voice leveling (an#315)
* **Type:**
  ```
  ``
  ```

  {“source”

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### pause *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Seconds of silence before this line, after the previous line’s speech
ends (the shot start, for the first line) — `(pause 1.5)` in `scene.md`.

#### planned_start(cursor)

Where this line starts, given the previous line ends at `cursor`.

The one rule the audio pipeline stamps into `start` and `an validate`
lays lines out by: `at` if set, else `cursor + pause`. A `start`
on a line never synthesized (no `audio_ref`) was authored — the
spelling of `at` before an#187 — and counts as one; a synthesized
line’s `start` is the pipeline’s own stamp, re-derived here.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> 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](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

seconds from the line’s start to the end of its
AUDIBLE speech — its take’s own trailing silence left out — stamped
with `duration` from the line’s audio. The next line’s `pause` counts
from here (a line with no pause still follows the whole take).
`None` (silent audio, a line its voice’s `trim_silence` already cut to
the tail the author keeps, a line stamped before an#397) counts from the
end of `duration`, 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), so `scene.json` and its fixtures do not change.

* **Type:**
  DERIVED (an#397)

#### word_timings *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[WordTimingIR](_autosummary/an.ir.schema.html.md#an.ir.schema.WordTimingIR)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The provider’s word timings, line-relative; `None` when the provider
has none (offline, Rhubarb) or the line was stamped before an#96.

### an.ir.schema.EXTENSION_TAG *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'extension'*

The union tag of the open member.

### *class* an.ir.schema.ExtensionAction(\*\*data)

Bases: `_ActionBase`

An action of a kind the core does not define: the IR’s one open member.

The schema’s `Action` union selects this for any `kind` other than the
core’s (ADR 0001 decision 2). When a genre has REGISTERED that kind
([`an.genres.registry.register_action_kind()`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_action_kind)), validating a document
yields the registered model instead — a `PlayAction` for `kind: play` —
so code downstream sees typed actions. Before registration the action
stays an `ExtensionAction`: its fields are kept as extras and round-trip
byte for byte, and [`resolved()`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction.resolved) (called by `flatten`, `an validate`
and 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.

```pycon
>>> ExtensionAction(kind="wave", target="flag").model_dump()
{'name': None, 'kind': 'wave', 'target': 'flag'}
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.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)

This action as its registered model.

A typed instance is returned as is. A bare `ExtensionAction` is
validated by the model its kind registered — or, unregistered, raises
[`UnregisteredKindError`](_autosummary/an.genres.registry.html.md#an.genres.registry.UnregisteredKindError) (`strict`) or
comes back unchanged (`strict=False`).

* **Return type:**
  [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)

### *class* an.ir.schema.LoopAction(\*\*data)

Bases: `_ActionBase`

Composition: repeat `child` `count` times.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.Meta(\*\*data)

Bases: `_IRModel`

Scene metadata.

#### captions *: [Captions](_autosummary/an.ir.schema.html.md#an.ir.schema.Captions) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Captions from the dialogue’s word timings ([`Captions`](_autosummary/an.ir.schema.html.md#an.ir.schema.Captions), an#175);
`None` — the default — is none, omitted from JSON like `style_pack`.

#### closing_transition *: [Transition](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition) | [None](https://docs.python.org/3/builtins/constants.html#None)*

a `fade` to its `color` over its
`duration`, 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 a `cut`) 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](https://docs.python.org/3/builtins/stdtypes.html#str) | [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The easing every authored `tween` that 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 like `style_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, `play` clips and swap channels have none to
inherit. There is no per-shot override yet — style is a scene’s.

#### default_renderer *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

a registered renderer’s name.

* **Type:**
  Like [`Shot.renderer`](_autosummary/an.ir.schema.html.md#an.ir.schema.Shot.renderer)

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### sounds *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[SoundCue](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue)]*

Sound cues in FILM time — a music bed, an ambience under every shot
([`SoundCue`](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue)). Empty, the default, is no sound layer at all.

#### step_hz *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Stepped timing for AUTHORED TWEENS, in pose updates per second; `None`
(the default) leaves every tween smooth. At 30 fps, `15` is “on twos”
and `10` “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 and `play` clips: only `tween` curves 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 (a `set` at an off-grid `at`
likewise lands where it was authored). A shot’s own `step_hz`
overrides this. Must be positive (schema) and `<= fps` (validate +
compile), an#89.

#### style_pack *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The `StylePack` in the project’s `styles` store 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 by
`an 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](_autosummary/an.ir.schema.html.md#an.ir.schema.VoiceLoudness) | [None](https://docs.python.org/3/builtins/constants.html#None)*

One loudness for every voice ([`VoiceLoudness`](_autosummary/an.ir.schema.html.md#an.ir.schema.VoiceLoudness), an#315); `None`
— the default — levels nothing, and is omitted from JSON.

### *class* an.ir.schema.Narration(\*\*data)

Bases: `_IRModel`

Off-screen narration. Same shape as Dialogue minus the speaker pin.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.ParallelAction(\*\*data)

Bases: `_ActionBase`

Composition: run all children simultaneously starting at the same time.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.Resolution(\*\*data)

Bases: `_IRModel`

Pixel dimensions of the rendered output.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.ir.schema.SPECIMEN_DURATION *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.5*

How long a specimen shot runs (`an library sheet` draws its first frame).

### an.ir.schema.SPECIMEN_SHOT_ID *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'specimen'*

The id of a specimen shot.

### an.ir.schema.STAGE_NODE_SPACE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'stage.node'*

The property space a 2D stage engine’s node lives in ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)).

### *class* an.ir.schema.SceneIR(\*\*data)

Bases: `_IRModel`

Top-level Scene IR document. The SSOT.

A document is portable, diffable, and renderer-agnostic. Persisted as JSON
at `ir/scene.json` inside an an project.

```pycon
>>> 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'
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.SequenceAction(\*\*data)

Bases: `_ActionBase`

Composition: run children one after the other.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.SetAction(\*\*data)

Bases: `_ActionBase`

Set a property to a value at a specific time. Discrete, no tween.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.Shot(\*\*data)

Bases: `_IRModel`

A single rendered unit. A scene is a sequence of shots.

A shot’s `renderer` selects the backend that draws it. Every renderer must accept the
same Shot fields; renderer-specific options go under `options`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### policy *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] | [None](https://docs.python.org/3/builtins/constants.html#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](https://docs.python.org/3/builtins/stdtypes.html#str)*

Which RENDERER draws this shot — not art direction. The field was
called `style` until an#106, colliding with the styles store (which
holds art direction) and with `AssetRef(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.
A `str` in the schema (ADR 0001 decision 2): any name a renderer
registered (`an.adapters.register_renderer`), checked by `an
validate`. `cutout` stays the persisted default (decision 9).

#### sounds *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[SoundCue](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue)]*

Sound cues in SHOT-local time ([`SoundCue`](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue)).

#### step_hz *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per-shot override of [`Meta.step_hz`](_autosummary/an.ir.schema.html.md#an.ir.schema.Meta.step_hz) (`None` = inherit).

#### transition *: [Transition](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition) | [None](https://docs.python.org/3/builtins/constants.html#None)*

How this shot is entered ([`Transition`](_autosummary/an.ir.schema.html.md#an.ir.schema.Transition)); `None` is a hard cut.

### *class* an.ir.schema.SoundCue(\*\*data)

Bases: `_IRModel`

One sound placed on the timeline: an SFX hit, an ambience, a music bed.

`sound` is a key in the project’s `sounds` store (`an.sounds`), where
the bytes and their licence live — the IR never inlines audio.

`at` is seconds from the start of whatever holds the cue: a shot’s
`sounds` are SHOT-local (they move with the shot, across transitions
and re-orderings), `meta.sounds` are FILM time (a music bed under the
whole thing).

```pycon
>>> SoundCue(sound="hit", at=1.2).gain_db
0.0
>>> SoundCue(sound="bed", loop=True, duck_db=-12).duck_db
-12.0
```

#### at *: [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[float](https://docs.python.org/3/builtins/functions.html#float), FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=0)])] | [CueAnchor](_autosummary/an.ir.schema.html.md#an.ir.schema.CueAnchor)*

Seconds, or a [`CueAnchor`](_autosummary/an.ir.schema.html.md#an.ir.schema.CueAnchor) resolved from the picture when the film
is laid out (an#317).

#### duck_db *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Attenuation, in dB, while any dialogue line plays; `None` never ducks.
A music bed usually wants `DEFAULT_DUCK_DB`; an SFX hit wants none.

#### duration *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#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`

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### until *: [CueUntil](_autosummary/an.ir.schema.html.md#an.ir.schema.CueUntil) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Ends the cue at another cue’s start ([`CueUntil`](_autosummary/an.ir.schema.html.md#an.ir.schema.CueUntil)), instead of a
fixed `duration`.

### *class* an.ir.schema.StagePlacement(\*\*data)

Bases: `_IRModel`

Where an entity stands on the stage, and how big it is.

```pycon
>>> StagePlacement(at=(120.0, -40.0), scale=0.5).at
(120.0, -40.0)
>>> StagePlacement().at is None
True
```

`at` is 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. `scale` multiplies 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. `depth` is 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 because `depth`’s reason still holds.

\*\*Deliberately only two fields, and the reason has been re-stated because
the first one expired.\*\* #108 sketched `depth` and `after` as 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_ROLES` refuses
by construction on the other side of the compiler.

What each would cost, so the next reader does not re-derive it:

- `depth` would place a prop on a parallax plane, which means parenting the
  prop into that plane’s subtree — and `_track_root_of` makes entity
  identity the first path segment, so every animation target on that prop
  changes shape.
- `after` would order a prop against planes, which is `characters_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](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

an environment
plane (`"set/skyline"`) or another placed entity (`"grid"`).
`None` = the default band, where the environment’s
`characters_after` cuts. 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](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[float](https://docs.python.org/3/builtins/functions.html#float), FieldInfo(annotation=NoneType, required=True, metadata=[AllowInfNan(allow_inf_nan=False)])], [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[float](https://docs.python.org/3/builtins/functions.html#float), FieldInfo(annotation=NoneType, required=True, metadata=[AllowInfNan(allow_inf_nan=False)])]] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`(x, y)` in scene pixels from the stage centre. `None` = default layout.

`allow_inf_nan=False` on both fields, and it is not pedantry: pydantic
serializes `inf` and `nan` to 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=0` already refuses `scale=0` and `scale=-1`; it
does not refuse `inf`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### scale *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Uniform scale multiplier on the built rig. `1.0` = the rig’s own size.

### *class* an.ir.schema.Transition(\*\*data)

Bases: `_IRModel`

How a shot is ENTERED — from the previous shot, or (for the first shot)
from nothing.

```pycon
>>> Transition(kind="dissolve", duration=0.5).duration
0.5
>>> Transition(kind="fade").color
'#000000'
```

- `cut` — the default, and what a shot with no `transition` means.
- `fade` — through `color`: the previous shot’s last `duration / 2`
  fades to the colour and this shot’s first `duration / 2` fades up from
  it. On the FIRST shot the whole `duration` is a fade up from the
  colour. **Holds the film’s length**: nothing overlaps.
- `dissolve` — the previous shot’s last `duration` seconds and this
  shot’s first `duration` seconds are seen through each other. \*\*The
  film gets `duration` shorter\*\* 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 in `direction` (the way the edge travels: `left` brings 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 frame `j` of `k`. Not on the first shot.

#### direction *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['left', 'right', 'up', 'down']*

A `wipe`’s direction; omitted from a dump for every other kind.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.TweenAction(\*\*data)

Bases: `_ActionBase`

Animate a property from a start value to an end value over a duration.

#### easing *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

a
tween that does not name an easing takes the scene’s
[`Meta.default_easing`](_autosummary/an.ir.schema.html.md#an.ir.schema.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 `.easing` still sees the curve a scene without a
default draws — and the serializer below omits an unset easing, so the
distinction survives `scene.json`. `None` is an explicit LINEAR
ramp (the evaluators’ reading of a null easing), not “unset”.

* **Type:**
  The curve. \*\*Unset is not the same as 

  ```
  ``
  ```

  ”ease_in_out”

  ```
  ``
  ```

  \*\* (an#166)

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.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)

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).

```pycon
>>> 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:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### *class* an.ir.schema.VisemeKeyframe(\*\*data)

Bases: `_IRModel`

A single mouth-shape keyframe in a viseme track.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.VisemeTrack(\*\*data)

Bases: `_IRModel`

Aligned viseme track produced by the lip-sync stage. Optional in P1.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.ir.schema.VoiceLoudness(\*\*data)

Bases: `_IRModel`

One 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’s `loudness_offset_db`), peaks held at `peak_db` dBFS by a
lookahead limiter — derived audio, content-keyed, never re-synthesised
([`an.audio.loudness`](_autosummary/an.audio.loudness.html.md#module-an.audio.loudness)). Unset, the default, levels nothing.

```pycon
>>> VoiceLoudness().target_lufs, VoiceLoudness().peak_db
(-16.0, -1.5)
>>> VoiceLoudness.model_validate(-20).target_lufs  # a bare number is the target
-20.0
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### peak_db *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The highest a sample may reach after the gain, dBFS.

#### target_lufs *: [float](https://docs.python.org/3/builtins/functions.html#float)*

speech for the web;
-23: EBU R128 broadcast).

* **Type:**
  The level every voice is brought to, LUFS (-16

### *class* an.ir.schema.WordTimingIR(\*\*data)

Bases: `_IRModel`

One word of a line and when it was spoken, in seconds from the line’s
start (like [`VisemeKeyframe`](_autosummary/an.ir.schema.html.md#an.ir.schema.VisemeKeyframe), never absolute). Stamped by the audio
pipeline from the provider’s word timings when it has them (an#96); JSON
only — `scene.md` never carries it, the way it never carries visemes.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.ir.schema.resolve_step_hz(shot, scene_step_hz)

The stepped-timing policy `shot` renders under: its own `step_hz`
when it declares one, else the scene’s, else `None` (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).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> 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)

A short shot showing the one stage entity `ref` casts, on its own (an#347).

* **Return type:**
  [`Shot`](_autosummary/an.ir.schema.html.md#an.ir.schema.Shot)

```pycon
>>> stage_specimen(AssetRef(kind="prop", id="lamp", store="props", ref="lamp")).entities[0].id
'lamp'
```

### an.ir.schema.unregistered_action_kind(kind, , where='')

The error for an action `kind` no loaded genre registered.

It names the installed genres whose declaration provides the kind, read
without loading them ([`an.genres.providers_of()`](_autosummary/an.genres.html.md#an.genres.providers_of)).

* **Return type:**
  [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)


# _autosummary/an.ir.validate.html.md

# an.ir.validate

Schema and semantic validation for SceneIR documents.

Two layers, called separately so callers can pick how strict to be:

- `validate_schema` — Pydantic validation only. Wrong types, missing required
  fields, malformed JSON.
- `validate_semantic` — cross-field checks. Unknown asset references,
  zero-duration shots, voice refs missing from a voices store.

Layout-overlap checks (boxes off-screen, text behind sprites) live in
`an.verify.layout`, not here, because they need a render context.

### Module Attributes

| [`RETIRED_KEYS`](_autosummary/an.ir.validate.html.md#an.ir.validate.RETIRED_KEYS)                 | Keys an#106 retired, and what to write instead.                                                                                                                                                                                                                                                                                                              |
|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`RETIRED_CAMERA_KEYS`](_autosummary/an.ir.validate.html.md#an.ir.validate.RETIRED_CAMERA_KEYS)          | an#109's removed camera fields.                                                                                                                                                                                                                                                                                                                              |
| [`CUT_CONTINUITY_PROPS`](_autosummary/an.ir.validate.html.md#an.ir.validate.CUT_CONTINUITY_PROPS)         | position in scene px, and the size of its scale (its SIGN is facing, which the genre's view continuity check says, an#203).                                                                                                                                                                                                                                  |
| [`CUT_CONTINUITY_TOLERANCE`](_autosummary/an.ir.validate.html.md#an.ir.validate.CUT_CONTINUITY_TOLERANCE)     | a pixel, or a hundredth of the scale.                                                                                                                                                                                                                                                                                                                        |
| [`DIALOGUE_OVERRUN_TOLERANCE_S`](_autosummary/an.ir.validate.html.md#an.ir.validate.DIALOGUE_OVERRUN_TOLERANCE_S) | a frame at 60 fps.                                                                                                                                                                                                                                                                                                                                           |
| [`POST_SYNTHESIS_CHECKS`](_autosummary/an.ir.validate.html.md#an.ir.validate.POST_SYNTHESIS_CHECKS)        | The registered checks whose answer depends on what synthesis produced — a line's real length, hence where it starts and ends — and that `an render` therefore runs again AFTER the audio pipeline, on the timing it will mux ([`post_synthesis_findings()`](_autosummary/an.ir.validate.html.md#an.ir.validate.post_synthesis_findings) runs exactly these, by name, through the registry). |

### Functions

| [`counter_block_ids`](_autosummary/an.ir.validate.html.md#an.ir.validate.counter_block_ids)(shot, stores)                   | The ids of `shot`'s counter text blocks (an#342): the only targets a `value` is authored on in the core.                                                                                                      |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`post_synthesis_findings`](_autosummary/an.ir.validate.html.md#an.ir.validate.post_synthesis_findings)(scene, \*[, fps, checks]) | `(check, finding)` for each finding the synthesized timing gives.                                                                                                                                             |
| [`registered_kind_problems`](_autosummary/an.ir.validate.html.md#an.ir.validate.registered_kind_problems)(scene)                   | The findings of the three registry checks alone — every action kind, entity kind and renderer the scene names must be registered — without the rest of `validate_semantic` (no stores, no rig builds; cheap). |
| [`require_registered_kinds`](_autosummary/an.ir.validate.html.md#an.ir.validate.require_registered_kinds)(scene, \*[, where])      | `scene`, or [`UnregisteredInSceneError`](_autosummary/an.ir.validate.html.md#an.ir.validate.UnregisteredInSceneError) naming every action kind, entity kind and renderer it uses that is not registered.                                      |
| [`rig_stores`](_autosummary/an.ir.validate.html.md#an.ir.validate.rig_stores)()                                      | `{entity kind: (mall store, descriptor kind)}` for every kind that has a rig.                                                                                                                                 |
| [`shot_dialogue_overruns`](_autosummary/an.ir.validate.html.md#an.ir.validate.shot_dialogue_overruns)(shot, \*[, ...])           | `(k, message)` for each line of `shot` that ends past the shot's end.                                                                                                                                         |
| [`validate_schema`](_autosummary/an.ir.validate.html.md#an.ir.validate.validate_schema)(doc)                              | Validate that `doc` (dict, JSON string, or SceneIR) conforms to the schema.                                                                                                                                   |
| [`validate_semantic`](_autosummary/an.ir.validate.html.md#an.ir.validate.validate_semantic)(scene, \*[, ...])               | Cross-field semantic checks.                                                                                                                                                                                  |

### Classes

| [`ValidationContext`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationContext)(scene, report, stores[, ...])   | What a registered semantic check ([`an.genres.SemanticCheck`](_autosummary/an.genres.html.md#an.genres.SemanticCheck)) reads.   |
|----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
| [`ValidationFinding`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationFinding)(severity, ir_path, description) | A single validation issue with a path into the IR.                                                                                           |
| [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)([passed, findings])              | Result of running one or more validators.                                                                                                    |

### Exceptions

| [`UnregisteredInSceneError`](_autosummary/an.ir.validate.html.md#an.ir.validate.UnregisteredInSceneError)(findings, \*[, where])   | A scene names kinds or renderers nothing registered: refused at load.   |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|

### an.ir.validate.CUT_CONTINUITY_PROPS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('x', 'y', 'scale_x', 'scale_y')*

position in
scene px, and the size of its scale (its SIGN is facing, which the genre’s
view continuity check says, an#203).

* **Type:**
  What an entity’s placement across a cut is compared on (an#394)

### an.ir.validate.CUT_CONTINUITY_TOLERANCE *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= {'scale_x': 0.01, 'scale_y': 0.01, 'x': 1.0, 'y': 1.0}*

a pixel, or
a hundredth of the scale.

* **Type:**
  How far a value may move across a cut and still be “the same”

### an.ir.validate.DIALOGUE_OVERRUN_TOLERANCE_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.016666666666666666*

a frame at 60 fps.

* **Type:**
  Slack before a line counts as running past its shot

### an.ir.validate.POST_SYNTHESIS_CHECKS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('dialogue_fits', 'dialogue_in_dissolve', 'cutout.hidden_mouth_while_speaking')*

The registered checks whose answer depends on what synthesis produced — a
line’s real length, hence where it starts and ends — and that `an render`
therefore runs again AFTER the audio pipeline, on the timing it will mux
([`post_synthesis_findings()`](_autosummary/an.ir.validate.html.md#an.ir.validate.post_synthesis_findings) runs exactly these, by name, through the
registry). A check added later that reads `Dialogue.duration` or `start`
belongs here; `tests/test_render_findings.py` lists the ones that do.

### an.ir.validate.RETIRED_CAMERA_KEYS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'focal_length', 'position', 'target'})*

an#109’s removed camera fields. A WARNING, not an error, and the difference
is the harm: a surviving `style` silently picks the wrong RENDERER, while
these three selected nothing — they described a 3D camera this package never
had. What is left is dead weight in a file, so it is worth saying and not
worth failing over.

Reported at all because the migration cannot reach them: a document already
at the current version is never migrated again, so a camera block that came
through a sync between the version bump and this check keeps them forever as
`extra="allow"` extras, and nothing else looks.

### an.ir.validate.RETIRED_KEYS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]]* *= {'meta': {'default_style': 'default_renderer'}, 'shot': {'style': 'renderer'}}*

Keys an#106 retired, and what to write instead. `SceneIR`’s models are
`extra="allow"` (deliberately — forward compatibility), so a document that
still carries one of these validates cleanly and renders with the DEFAULT
renderer. The migration rewrites stored 0.1.x documents, but nothing rewrites
a document that is already 0.2.0: an agent patch, a hand edit, or a caller
passing `style=` to `Shot(...)` all produce a permanently dead key that no
later migration will touch. So it is caught here, at ERROR, by name.

### *exception* an.ir.validate.UnregisteredInSceneError(findings, , where='')

Bases: [`UnregisteredKindError`](_autosummary/an.genres.registry.html.md#an.genres.registry.UnregisteredKindError)

A scene names kinds or renderers nothing registered: refused at load.

Raised by [`require_registered_kinds()`](_autosummary/an.ir.validate.html.md#an.ir.validate.require_registered_kinds) — what `an.load(project)`
(and so `an render`) runs, so a typo’d `kind:` or `renderer:` can no
longer render silently wrong now that the schema holds them as `str`
(review-244 S2). `findings` keeps each one with its IR path.

### *class* an.ir.validate.ValidationContext(scene, report, stores, voices=None, characters=None, sounds=None, library_lock=None, shot=None, index=None, memo=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a registered semantic check ([`an.genres.SemanticCheck`](_autosummary/an.genres.html.md#an.genres.SemanticCheck)) reads.

`stores` holds only the stores actually supplied, keyed by MALL name (an
absent one means its checks did not RUN — never that what it holds is
missing). `shot` and `index` are set while the `shot` stage runs.
`memo` is shared by every check of one `validate_semantic` call, so
two checks that need the same derived fact compute it once
([`cached()`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationContext.cached)).

#### cached(key, compute)

`compute()`, once per `key` per validation.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

#### library_lock *: [Mapping](https://docs.python.org/3/library/typing.html#typing.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

The project’s asset-library lockfile (`mall["library_lock"]`, an#240);
`None` means the pin checks did not run.

#### *property* path *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The IR path of the current shot (`timeline/<index>`).

### *class* an.ir.validate.ValidationFinding(severity, ir_path, description, location=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single validation issue with a path into the IR.

#### location *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`"<file>:<line>"` when the thing to fix is an opaque source a shot runs
(a Manim scene file, an#279) rather than the IR; `None` otherwise.

### *class* an.ir.validate.ValidationReport(passed=True, findings=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of running one or more validators.

`passed` is True iff there are no error-severity findings.

### an.ir.validate.counter_block_ids(shot, stores)

The ids of `shot`’s counter text blocks (an#342): the only targets a
`value` is authored on in the core. Empty without the props store.

* **Return type:**
  [`frozenset`](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.ir.validate.post_synthesis_findings(scene, , fps=None, checks=('dialogue_fits', 'dialogue_in_dissolve', 'cutout.hidden_mouth_while_speaking'), \*\*stores)

`(check, finding)` for each finding the synthesized timing gives.

The SAME registered checks `an validate` runs, selected by name
([`POST_SYNTHESIS_CHECKS`](_autosummary/an.ir.validate.html.md#an.ir.validate.POST_SYNTHESIS_CHECKS)): dialogue past its shot’s end, a speaker
overlapping themself, a line heard during a dissolve, a line spoken while
the speaker’s view hides its mouth. `an render` calls this once the audio
pipeline has stamped every line’s real `duration`, so what `an validate`
could only estimate is reported exactly, at the moment it becomes known
(an#254). `fps` is the render’s (it decides the dissolve overlaps);
default the scene’s. `stores` are [`validate_semantic()`](_autosummary/an.ir.validate.html.md#an.ir.validate.validate_semantic)’s
`available_*` keywords. A check no loaded genre registered is skipped.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`ValidationFinding`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationFinding)]]

### an.ir.validate.registered_kind_problems(scene)

The findings of the three registry checks alone — every action kind,
entity kind and renderer the scene names must be registered — without the
rest of `validate_semantic` (no stores, no rig builds; cheap).

* **Return type:**
  [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)

### an.ir.validate.require_registered_kinds(scene, , where='')

`scene`, or [`UnregisteredInSceneError`](_autosummary/an.ir.validate.html.md#an.ir.validate.UnregisteredInSceneError) naming every action
kind, entity kind and renderer it uses that is not registered.

* **Return type:**
  [`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR)

### an.ir.validate.rig_stores()

`{entity kind: (mall store, descriptor kind)}` for every kind that has a rig.

The core’s own (`prop`) plus the kinds genres register with a
`descriptor_kind` (the cut-out genre’s `character`): derived from the
registry, not a table the core edits (an#246).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> rig_stores()["prop"]
('props', 'PropDescriptor')
```

### an.ir.validate.shot_dialogue_overruns(shot, , effects_of=None, synthesized_only=False, tolerance_s=0.016666666666666666)

`(k, message)` for each line of `shot` that ends past the shot’s end.

The ONE overrun check: `an validate` runs it, the audio pipeline runs it
after synthesis ([`an.audio.pipeline.dialogue_overruns()`](_autosummary/an.audio.pipeline.html.md#an.audio.pipeline.dialogue_overruns)), and `an
render` reports it from the rendered timing ([`post_synthesis_findings()`](_autosummary/an.ir.validate.html.md#an.ir.validate.post_synthesis_findings)).

The audio is cut at the shot end (each shot’s mix is trimmed to its
duration), and the lines play back to back, so a shot shortened below its
dialogue loses the tail of it — silently, until now (an e2e run shrank an
8.2 s shot holding 7.1 s of speech to 3.0 s and `an validate` said nothing).

What is known depends on when this runs. After the audio pipeline, a line
carries its real `duration` and the check is exact. Before it, the
duration is the offline voice’s estimate
([`an.audio.offline_tts.estimate_speech_duration()`](_autosummary/an.audio.offline_tts.html.md#an.audio.offline_tts.estimate_speech_duration) over the voice’s
`tempo` — exactly what an offline render will give, and an
under-estimate for a real voice). Either
way the lines are laid out by the pipeline’s own rule,
[`an.ir.schema.Dialogue.planned_start()`](_autosummary/an.ir.schema.html.md#an.ir.schema.Dialogue.planned_start) — back to back from the shot
start, shifted by each line’s `pause` or pinned by its `at` (an#187) —
so a pause edited after synthesis is judged where it will play, not where
the stale stamp says. `effects_of` (

```
``
```

line -> \`\` its voice’s normalised
effects) supplies the tempo, and — for a synthesized line whose voice does
not trim — the fix of trimming the silence a real voice pads a line with.

```pycon
>>> from an.ir.schema import Dialogue, Shot
>>> shot = Shot(id="s", duration=1.0, dialogue=[
...     Dialogue(speaker="a", text="hi", start=0.2, duration=1.3, audio_ref="k")])
>>> [(k, m[:44]) for k, m in shot_dialogue_overruns(shot)]
[(0, 'line 0 (a) ends at 1.30s as synthesized, pas')]
```

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### an.ir.validate.validate_schema(doc)

Validate that `doc` (dict, JSON string, or SceneIR) conforms to the schema.

* **Return type:**
  [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)

```pycon
>>> 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.ir.validate.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)

Cross-field semantic checks. Pass live stores in for cross-store checks.

Both `available_voices` and `available_characters` accept any mapping;
`available_props` is the same thing for `kind="prop"` entities (an#108),
and `available_environments` lets 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 and `play` checks read descriptor
dicts, an#87 / an#7). Pass `None` to skip those checks — and know that
skipping them is what it sounds like: a `play` or a swap the compiler
will refuse passes silently without the store (the CLI, `an validate`,
always passes it).

`available_styles` is the project’s `styles` store: a check reads the
StylePack the scene names (`meta.style_pack`) from `ctx.stores["styles"]`
(a style’s `policy`, an#348).

`available_library_lock` is the project’s asset-library lockfile
(`mall["library_lock"]`): with it, every scene `library:` pin is checked
against the lockfile (`warning` on disagreement) and every pinned
check-out against its library version (`info` when it has been edited).

The checks are a REGISTRY ([`an.genres.registry.register_check()`](_autosummary/an.genres.registry.html.md#an.genres.registry.register_check)):
the core’s own register below, a genre’s when it is loaded (the cut-out
genre’s `play`, `expression`, turn and view checks), and they run in
stages — `scene`, then `shot` once per shot, then `finish` — each by
its `order`. 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.

`only` runs just the registered checks of those names (what `an render`
does after synthesis, [`post_synthesis_findings()`](_autosummary/an.ir.validate.html.md#an.ir.validate.post_synthesis_findings)); `fps` is 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:**
  [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)


# _autosummary/an.iterate.html.md

# an.iterate

Iterative edit loop — free-text instruction → IR patch via Claude → re-render.

Phase 10. The spec’s signature user story:

> > “I say ‘make Maya’s laugh longer and warmer.’ Claude Code edits the IR,
> > re-renders just the affected shot, and shows me the diff. I approve.”

This module turns a free-text instruction into a structured set of patches
on the SceneIR JSON tree, validates them, applies them, persists, and (optionally)
re-renders. The vision LM is Claude Opus 4.7 with adaptive thinking;
`messages.parse()` + a Pydantic schema guarantees the patches are valid
JSON of the expected shape.

Path syntax for patches: slash-delimited JSON-pointer-style. List indices
are integers. Examples:

> “meta/title”
> “timeline/0/duration”
> “timeline/1/dialogue/0/text”
> “timeline/1/dialogue/0/emotion”

Patch operations:

> {“op”: “set”, “path”: “…”, “value”: …}     # replace (or create) value
> {“op”: “append”, “path”: “…”, “value”: …}  # append to a list
> {“op”: “delete”, “path”: “…”}                # remove an entry

The orchestrator records each iteration in `mall["decisions"]` so the
agent can review what changed across runs.

### Functions

| [`iterate`](_autosummary/an.iterate.html.md#an.iterate.iterate)(project_dir, instruction, \*[, ...])   | Apply a free-text instruction to the scene at `project_dir`.                  |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`system_prompt`](_autosummary/an.iterate.html.md#an.iterate.system_prompt)()                                | The `an iterate` system prompt: the protocol around the generated vocabulary. |

### Classes

| [`IterateResponse`](_autosummary/an.iterate.html.md#an.iterate.IterateResponse)(\*\*data)                       | Structured reply from Claude for a single iterate() call.   |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| [`IterateResult`](_autosummary/an.iterate.html.md#an.iterate.IterateResult)([success, summary, patches, ...]) | Outcome of an iterate() call.                               |
| [`Patch`](_autosummary/an.iterate.html.md#an.iterate.Patch)(\*\*data)                                 | A single mutation against the SceneIR JSON tree.            |

### Exceptions

| [`IterateError`](_autosummary/an.iterate.html.md#an.iterate.IterateError)   | Raised when an iterate call cannot apply its proposed patches.   |
|-----------------------------------------------------------------|------------------------------------------------------------------|

### *exception* an.iterate.IterateError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when an iterate call cannot apply its proposed patches.

### *class* an.iterate.IterateResponse(\*\*data)

Bases: `BaseModel`

Structured reply from Claude for a single iterate() call.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.iterate.IterateResult(success=True, summary='', patches=<factory>, affected_shots=<factory>, new_scene=None, validation=None, error=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of an iterate() call.

### *class* an.iterate.Patch(\*\*data)

Bases: `BaseModel`

A single mutation against the SceneIR JSON tree.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.iterate.iterate(project_dir, instruction, , apply=True, model='claude-opus-4-7', max_tokens=4096)

Apply a free-text instruction to the scene at `project_dir`.

Steps:

> 1. Load the project.
> 2. Send the current scene + the instruction to Claude.
> 3. Parse the structured patch response.
> 4. Apply patches to a deep-copied IR; validate.
> 5. If valid and `apply=True`, persist to mall[“scenes”][“main”] and
>    append to mall[“decisions”].

The caller is responsible for re-rendering: `an render` then re-renders
exactly the shots whose content changed, found by digest (the shot cache,
ADR 0004) — `IterateResult.affected_shots` is the model’s account of
which those are, for the reader, not the mechanism.

* **Return type:**
  [`IterateResult`](_autosummary/an.iterate.html.md#an.iterate.IterateResult)

### an.iterate.system_prompt()

The `an iterate` system prompt: the protocol around the generated vocabulary.

Loads the installed genres first (their presets and methods are part of
the vocabulary). Stable for a given registry, so it caches as well as the
hand-written one did.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> text = system_prompt()
>>> "walk" in text and "Rules:" in text
True
```


# _autosummary/an.library.affordances.html.md

# an.library.affordances

Affordances for the library: the capability registry, re-exported from [`an.capabilities`](_autosummary/an.capabilities.html.md#module-an.capabilities).

P5 seeded ADR 0002’s `affordances(asset)` here; P7 moved the tables and the
matcher to the core registry, [`an.capabilities`](_autosummary/an.capabilities.html.md#module-an.capabilities), and this module now
re-exports the SAME objects (`ANALYSERS is an.capabilities.ANALYSERS`), so the
library’s facets and `find(…, near=True)` and the compiler’s method choice
are one derivation and one matcher, never two.

The character analyser is the cut-out genre’s: it registers through
`an.genres.cutout.CUTOUT` (its `capabilities` and `analysers`
fields) when the genres load, which the library’s entry points do
([`an.library.api.publish()`](_autosummary/an.library.api.html.md#an.library.api.publish), [`an.library.api.find()`](_autosummary/an.library.api.html.md#an.library.api.find)).

```pycon
>>> import an.capabilities
>>> ANALYSERS is an.capabilities.ANALYSERS and CAPABILITIES is an.capabilities.CAPABILITIES
True
>>> afford = {"swap.view": {"keys": ["front", "side"]}, "limbs.legs": {}}
>>> matches(afford, "swap.view:side"), matches(afford, "swap.view:back"), matches(afford, "limbs.legs")
(True, False, True)
```

### Functions

| [`analyse`](_autosummary/an.library.affordances.html.md#an.library.affordances.analyse)(kind, doc[, art])                         | `(profile, analysers)` of one subject: its capabilities and the analyser versions used.   |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`capability_of`](_autosummary/an.library.affordances.html.md#an.library.affordances.capability_of)(query)                              | `(capability, key)` of a query term (the library's `find` form).                          |
| [`current_affordances`](_autosummary/an.library.affordances.html.md#an.library.affordances.current_affordances)(kind, doc, art, \*, ...)      | The stored snapshot when its analyser version is current, else a fresh derivation.        |
| [`matches`](_autosummary/an.library.affordances.html.md#an.library.affordances.matches)(profile, term)                            | Whether `profile` (or the subject of `term` in a `Subjects`) meets `term`.                |
| [`missing`](_autosummary/an.library.affordances.html.md#an.library.affordances.missing)(profile, requires)                        | The requirement terms `profile` does not meet, spelled, in the order asked.               |
| [`register_analyser`](_autosummary/an.library.affordances.html.md#an.library.affordances.register_analyser)(kind, \*[, version, ...])       | Register an analyser.                                                                     |
| [`register_capability`](_autosummary/an.library.affordances.html.md#an.library.affordances.register_capability)(name, \*[, description, ...]) | Register a capability (or a [`Capability`](_autosummary/an.library.affordances.html.md#an.library.affordances.Capability)). |
| [`remedy_for`](_autosummary/an.library.affordances.html.md#an.library.affordances.remedy_for)(term)                                  | What would add the capability a requirement term asks for.                                |

### Classes

| [`Analyser`](_autosummary/an.library.affordances.html.md#an.library.affordances.Analyser)(kind, version, derive[, subject, ...])   | The derivation of one kind's profile, versioned.                     |
|----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`Capability`](_autosummary/an.library.affordances.html.md#an.library.affordances.Capability)(name, description, remedy[, ...])      | A registered capability: its name, what it means, and how to add it. |

### *class* an.library.affordances.Analyser(kind, version, derive, subject='asset', declares=(), overrides=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The derivation of one kind’s profile, versioned.

`kind` is an asset kind (`character`) or a subject that has one
analyser (`engine`, `environment`). Bump `version` whenever the
output can change for the same input: snapshots made under the old one are
then recomputed on read. The version is NOT a compile input (consult §5):
the derived profile is, so touching an analyser without changing its output
re-renders nothing.

#### declares *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ()*

The document’s declared facts the derivation honours instead of deriving
(`rest_view`, `face_overlay`) or reads as a request (`gait`):
reported by `describe_asset` (ADR 0002 decision 2).

#### overrides *: [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[Any](https://docs.python.org/3/library/typing.html#typing.Any), [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]], [Iterable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]] | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

`(doc, art) -> declared field names` the derivation USED as overrides
that do not show on an afforded capability’s params: a declared fact
that REMOVES a capability (a character’s `occluded`, an#381). Those on
an afforded capability are already in its `overrides` param.
`None`: none beyond those. Reporting only: it changes no profile, so
adding it does not bump `version`.

### *class* an.library.affordances.Capability(name, description, remedy, subject='asset', command=None, version='1')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A registered capability: its name, what it means, and how to add it.

`command` is the CLI that adds it, when one exists (`an character
add-views`); `version` bumps when the *meaning* of the name changes (a
persisted name is never redefined in place).

#### to_json()

The capability as the generated docs and the MCP surface list it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.library.affordances.analyse(kind, doc, art=None)

`(profile, analysers)` of one subject: its capabilities and the analyser versions used.

A kind with no registered analyser affords nothing *derived* and records no
analyser — an honest empty answer, not a guess.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> analyse("no-such-kind", {})
({}, {})
```

### an.library.affordances.capability_of(query)

`(capability, key)` of a query term (the library’s `find` form).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

```pycon
>>> capability_of("swap.view:side"), capability_of("limbs.legs")
(('swap.view', 'side'), ('limbs.legs', None))
```

### an.library.affordances.current_affordances(kind, doc, art, , stored, stored_analysers)

The stored snapshot when its analyser version is current, else a fresh derivation.

Affordances are derived data, so recomputing them is always safe; trusting a
snapshot made by an older analyser is what would make a facet lie.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.library.affordances.matches(profile, term)

Whether `profile` (or the subject of `term` in a `Subjects`) meets `term`.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### an.library.affordances.missing(profile, requires)

The requirement terms `profile` does not meet, spelled, in the order asked.

THE matcher (ADR 0002 decision 4): `why_not`, `applicable`,
`resolve` and the library’s `find(…, near=True)` all call it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> missing({}, ["env.ffmpeg"]), missing(Subjects(environment={"env.ffmpeg": {}}), ["env.ffmpeg"])
(['env.ffmpeg'], [])
```

### an.library.affordances.register_analyser(kind, , version='', subject='asset', owner='an')

Register an analyser. Two forms.

`register_analyser(Analyser(...), owner=...)` registers the object and
returns it (a genre’s `analysers` field goes this way). With a `kind`
string it is a decorator: `@register_analyser("character", version="0.1.0")`
registers the decorated derivation.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.library.affordances.register_capability(name, , description='', remedy='', subject='asset', command=None, version='1', owner='an')

Register a capability (or a [`Capability`](_autosummary/an.library.affordances.html.md#an.library.affordances.Capability)). Returns it.

Re-registering the same definition is a no-op; a different definition
under a name another owner holds raises, because capability names are
persisted and two meanings for one name would make a stored facet lie.

* **Return type:**
  [`Capability`](_autosummary/an.capabilities.html.md#an.capabilities.Capability)

```pycon
>>> cap = register_capability("demo.thing", description="a thing", remedy="add one", owner="demo")
>>> CAPABILITIES["demo.thing"] is cap
True
>>> _ = drop_owner("demo")
```

### an.library.affordances.remedy_for(term)

What would add the capability a requirement term asks for.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> remedy_for("no.such.capability")
'no registered capability no.such.capability; see vocabulary() for the known names'
```


# _autosummary/an.library.api.html.md

# an.library.api

The library’s verbs: `publish`, `find`, `vocabulary`, `show`, `promote`, `retire`.

Plain functions over [`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library) objects (pillar 8:
the functions are the API; the `an library …` CLI and, later, MCP are thin
projections of them). Writes take the one owning library; reads take a library
or a search path ([`search_path()`](_autosummary/an.library.federation.html.md#an.library.federation.search_path)). Check-out lives
in [`an.library.checkout`](_autosummary/an.library.html.md#an.library.checkout).

The documents they write (ADR 0005 decision 4, design §4):

- a **record** per asset — identity and curation, mutable: `id`, `kind`,
  `title`, `family`, `head`, `status` (`retired` hides it from
  `find`: [`retire()`](_autosummary/an.library.api.html.md#an.library.api.retire)), its append-only `status_history`, `facets`
  (`style`, `origin`), `tags`;
- a **label** per relabel of a version’s unchanged content (an#307) —
  append-only, keyed by the hash of what it says: the source, who and why,
  and the rights it gave the version; a version’s rights read its labels;
- a **version** per publish — immutable: the descriptor `doc` verbatim, its
  `files` as `dol.content.ContentRef` s, the asset-level `source`,
  `derived_from`, the derived `affordances` with the `analysers` that made
  them, the rolled-up `rights`, the `art` facet, and `manifest_sha256` —
  the **version identity**: the hash of `doc`, the files’ hashes, `source`
  and `derived_from`. It is not a content key (lineage changes it, and a
  checked-out copy can be edited after it is pinned); nothing should cache on it
  as one. Publishing what the head already is makes no new version.

**Rights are recomputed, not trusted.** The stored `rights` block is a cache of
[`effective_rights()`](_autosummary/an.library.api.html.md#an.library.api.effective_rights), which rolls up — most restrictive wins — the asset-level
source, the descriptor’s own source, every part’s, and recursively every version
the asset derives from. `promote` and `find(rights=…)` recompute it.

### Module Attributes

| [`LIBRARY_ERRORS`](_autosummary/an.library.api.html.md#an.library.api.LIBRARY_ERRORS)   | the CLI prints these as a sentence and exits non-zero.   |
|-------------------------------------------------------------------|----------------------------------------------------------|

### Functions

| [`effective_rights`](_autosummary/an.library.api.html.md#an.library.api.effective_rights)(libraries, version, \*[, ...])    | The rights of a version, recomputed from its sources, its lineage and its bytes.      |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| [`find`](_autosummary/an.library.api.html.md#an.library.api.find)(libraries, \*[, kind, style, affords, ...])   | Assets matching every facet given (AND across facets, OR within one facet's values).  |
| [`promote`](_autosummary/an.library.api.html.md#an.library.api.promote)(libraries, ref, \*[, to, as_id, ...])      | Copy one version into another library — by default the core `an` library.             |
| [`publish`](_autosummary/an.library.api.html.md#an.library.api.publish)(library, asset_id, doc[, files, ...])      | Publish `doc` and its `files` as the next version of `asset_id` in `library`.         |
| [`publish_dir`](_autosummary/an.library.api.html.md#an.library.api.publish_dir)(library, folder, asset_id, \*\*kwargs) | Publish an asset folder as it sits in a project store (`assets/characters/alice/`).   |
| [`reindex`](_autosummary/an.library.api.html.md#an.library.api.reindex)(library, \*[, search])                     | Rebuild `library`'s floor index from its versions.                                    |
| [`retire`](_autosummary/an.library.api.html.md#an.library.api.retire)(library, asset_id, \*, by, reason)          | Retire an asset id: recorded, hidden from `find` by default, never deleted.           |
| [`scan_index`](_autosummary/an.library.api.html.md#an.library.api.scan_index)(library)                                | Every asset's head version in `library`, read from the stores.                        |
| [`set_status`](_autosummary/an.library.api.html.md#an.library.api.set_status)(library, asset_id, status, \*, by, ...) | Set an asset's curation status, recording who and why; return the record.             |
| [`show`](_autosummary/an.library.api.html.md#an.library.api.show)(libraries, ref)                               | The record, the resolved version, its recomputed rights and the list of versions.     |
| [`unknown_advice`](_autosummary/an.library.api.html.md#an.library.api.unknown_advice)(libraries, version, \*[, ...])      | What would answer each `unknown` contributor of a version — one sentence per kind.    |
| [`version_labels`](_autosummary/an.library.api.html.md#an.library.api.version_labels)(library, version)                   | The labels recorded on a stored version since it was published, oldest first.         |
| [`version_sources`](_autosummary/an.library.api.html.md#an.library.api.version_sources)(libraries, version, \*[, ...])     | Every labelled source a version's rights depend on — its own, its lineage, its bytes. |
| [`vocabulary`](_autosummary/an.library.api.html.md#an.library.api.vocabulary)(libraries, \*[, index])                 | Every facet with its values and counts, and the registered capabilities.              |

### Classes

| [`FindResult`](_autosummary/an.library.api.html.md#an.library.api.FindResult)(hits, near, counts)                    | Hits, near misses (with `near=True`), and per-facet value counts over the hits.   |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`Hit`](_autosummary/an.library.api.html.md#an.library.api.Hit)(library, asset_id, version, score[, ...])     | One asset that answers a query — or nearly does (`missing` non-empty).            |
| [`IndexEntry`](_autosummary/an.library.api.html.md#an.library.api.IndexEntry)(library, asset_id, version, kind, ...) | One asset's head version, as the index sees it.                                   |
| [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)(ref, manifest_sha256, created, ...) | What a publish did: the version it names, and whether it made one.                |

### Exceptions

| [`CheckoutError`](_autosummary/an.library.api.html.md#an.library.api.CheckoutError)         | A version cannot be materialised into this project as asked.                     |
|------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`IntegrityError`](_autosummary/an.library.api.html.md#an.library.api.IntegrityError)        | Stored bytes, paths or a stored manifest do not match what was recorded.         |
| [`LibraryError`](_autosummary/an.library.api.html.md#an.library.api.LibraryError)          | A library operation refused, with a sentence saying why and what to do.          |
| [`LibraryIndexWarning`](_autosummary/an.library.api.html.md#an.library.api.LibraryIndexWarning)   | A record or version could not be read; the index skipped it.                     |
| [`PlaceholderRigWarning`](_autosummary/an.library.api.html.md#an.library.api.PlaceholderRigWarning) | A character published with no rig: the compiler would draw only its placeholder. |

### *exception* an.library.api.CheckoutError

Bases: [`LibraryError`](_autosummary/an.library.api.html.md#an.library.api.LibraryError)

A version cannot be materialised into this project as asked.

### *class* an.library.api.FindResult(hits, near, counts)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Hits, near misses (with `near=True`), and per-facet value counts over the hits.

#### to_dict()

A JSON-ready view.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* an.library.api.Hit(library, asset_id, version, score, title=None, license_class='unknown', missing=<factory>, remedies=<factory>, federated=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One asset that answers a query — or nearly does (`missing` non-empty).

#### *property* ref *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

namespaced when the search spanned several libraries.

* **Type:**
  The reference to pin

#### to_dict()

A JSON-ready view (CLI `--json`, MCP).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* an.library.api.IndexEntry(library, asset_id, version, kind, record, rights, affordances, art, document=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One asset’s head version, as the index sees it.

#### affordance_terms()

Every query term this entry satisfies: `cap` and each `cap:key`.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### facet_values()

Every facet’s values for this entry (the AND/OR matcher’s input).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### *exception* an.library.api.IntegrityError

Bases: [`LibraryError`](_autosummary/an.library.api.html.md#an.library.api.LibraryError)

Stored bytes, paths or a stored manifest do not match what was recorded.

### an.library.api.LIBRARY_ERRORS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[type](https://docs.python.org/3/builtins/functions.html#type)[[BaseException](https://docs.python.org/3/builtins/exceptions.html#BaseException)], ...]* *= (<class 'an.library.api.LibraryError'>, <class 'an.library.ids.AssetIdError'>, <class 'an.library.federation.AssetNotFoundError'>, <class 'an.library.stores.VersionExistsError'>, <class 'an.library.rights.RightsRefusal'>, <class 'an.library.kinds.UnknownKindError'>, <class 'an.library.registry.RegistryError'>)*

the CLI
prints these as a sentence and exits non-zero.

* **Type:**
  Every error a library verb raises for a caller’s mistake (not a bug)

### *exception* an.library.api.LibraryError

Bases: [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)

A library operation refused, with a sentence saying why and what to do.

### *exception* an.library.api.LibraryIndexWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A record or version could not be read; the index skipped it.

### *exception* an.library.api.PlaceholderRigWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A character published with no rig: the compiler would draw only its placeholder.

### *class* an.library.api.PublishResult(ref, manifest_sha256, created, rights, affordances, advice=(), conflicts=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a publish did: the version it names, and whether it made one.

#### advice *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ()*

what would answer each kind of gap, one
sentence each ([`unknown_advice()`](_autosummary/an.library.api.html.md#an.library.api.unknown_advice)).

* **Type:**
  For an `unknown` result

#### conflicts *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[RightsConflict, ...]* *= ()*

statements freer
than what binds their bytes, kept beside the stricter one that binds.

* **Type:**
  The rights conflicts the version records (an#357)

### an.library.api.effective_rights(libraries, version, \*, floor=<object object>, owner=None, rule=None)

The rights of a version, recomputed from its sources, its lineage and its bytes.

owner: the library holding `version` (see [`version_sources()`](_autosummary/an.library.api.html.md#an.library.api.version_sources))
rule: a `_PerFileRule` to share with the caller (one per operation)

* **Return type:**
  [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights)

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.vase", {"name": "vase"},
...             source={"provider": "film", "license": "all-rights-reserved"})
>>> effective_rights(lib, read_version(lib, "prop.vase", "v001"), owner=lib).license_class
'private'
```

### an.library.api.find(libraries, \*, kind=None, style=None, affords=None, rights='any', family=None, origin=None, status=None, tags=None, art=None, near=False, index=<function scan_index>)

Assets matching every facet given (AND across facets, OR within one facet’s values).

* **Return type:**
  [`FindResult`](_autosummary/an.library.api.html.md#an.library.api.FindResult)

affords: capabilities the asset must ALL have — `limbs.legs`, or
: `swap.view:side` for a capability with a given key. Each capability is
  its own boolean facet, so a list of them is AND, as across facets. An
  unregistered name raises, naming the close ones

rights: `any` (default — study renders are legitimate), `publishable`
: (`free` + `attribution` + `noncommercial`), `commercial`
  (`free` + `attribution`: what a commercial video may use, an#373),
  or licence classes. Rights are recomputed
  from each version’s sources and lineage, not read from its cache

status: curation statuses; an asset whose status is hidden
: (`HIDDEN_STATUSES`: `retired`) is offered only when its status
  is asked for by name

near: also return assets that pass every other facet but miss some
: capabilities, or are curated for another style than asked, each with
  what is missing and the remedy that would add it (a style mismatch is
  listed as `style:<wanted>`, remedied by restyling: an#271)

index: the index to read (default: a scan of the stores)

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.lamp", {"name": "lamp"}, style=["reiniger", "gilliam"])
>>> [h.ref for h in find(lib, kind="prop", style="gilliam")]
['prop.lamp@v001']
>>> len(find(lib, kind="prop", rights="publishable"))  # no source: unknown
0
```

### an.library.api.promote(libraries, ref, , to=None, as_id=None, allow_restricted=False, strict_assets=False)

Copy one version into another library — by default the core `an` library.

An asset made in a genre’s library and reused across genres is promoted to the
core (plan §1 decision 7). The copy is a new version in the target, derived
from the source version, with the record’s curation carried over.

- A `private` or `unknown` version is refused unless
  `allow_restricted=True`: private-study material never leaves its library
  by default (ADR 0005 decision 10). The rights checked are recomputed from
  the version’s sources and lineage, and the stricter of those and the
  stored ones wins.
- If the target already has an asset with this id that does not derive from
  the one promoted (another character that happens to share the id), it is
  refused: promoting would make it a new version of an unrelated asset.
  `as_id` promotes under another id.
- `strict_assets`: refuse a copy that would record a rights conflict
  ([`publish()`](_autosummary/an.library.api.html.md#an.library.api.publish)’s, an#357), instead of recording it.

* **Return type:**
  [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)

### an.library.api.publish(library, asset_id, doc, files=None, \*, source=None, relicense=None, relabel=None, derived_from=(), title=None, family=None, style=None, origin=None, status=None, tags=None, replace_curation=False, note=None, expect_head=<object object>, search=None, carry_source=True, license_parts=None, file_sources=None, strict_assets=False)

Publish `doc` and its `files` as the next version of `asset_id` in `library`.

library: the owning library (writes never go to a search path)
asset_id: `<kind>.<slug>`; the kind must be registered
doc: the descriptor — the existing document `an` already versions

> (`CharacterDescriptor`, …), as a model or its JSON dict, stored verbatim

files: the asset’s files, by the relative paths the descriptor uses
: (`parts/head.svg`) — stored once each in the content-addressed blobs

source: provenance declared for the asset as a whole. It contributes BESIDE
: the descriptor’s own `source` (the most restrictive wins), never
  instead of it. With no `source`, and a descriptor declaring nothing
  or exactly what the head’s declared, the source of the previous version
  carries forward (`carry_source`) — for the bytes it was declared on
  only: a file changed or added since is recorded as `unlabelled` on the
  version and is `unknown`. Later versions inherit that gap through
  `previous` even when they pass `source=`; `relabel` (or a
  relicence) answers it. With no source at all the version is `unknown`
  — recorded and visible, not refused

relicense: `{"by": who, "reason": why}` — the ONLY way to relax rights.
: Rights attach to the bytes and the lineage: a new version inherits the
  version it follows (`previous`), every version it derives from, and
  every version holding the same file bytes, and may only be more
  restrictive than they are — a cc0 in the descriptor or in `source=`
  never relabels private art. A relicence makes `source` (required) the
  whole statement, records who and why on the version (and in its
  manifest and reasons), and covers these bytes for later versions

relabel: `{"by": who, "reason": why}` beside an explicit `source=`
: (required) — a first statement about bytes NOBODY labelled (an#263):
  the gaps of this asset’s own version chain (files an earlier version
  recorded `unlabelled`, files no person’s source spoke for, an earlier
  version with no source at all) are answered with `source`, and so is
  another asset’s silence about a file the chain held once and this
  version no longer holds (an#307). It relaxes no statement anyone made:
  a private (or any) licence, a per-part source, a version this one
  derives from, and every other asset’s statement about bytes this
  version holds still bind. On content that changed, recorded on the new
  version, in its manifest and in its reasons; on UNCHANGED content,
  recorded on the head in the append-only `labels` store
  ([`version_labels()`](_autosummary/an.library.api.html.md#an.library.api.version_labels)) and no version is minted (an#307)

derived_from: library references this version derives from (an earlier version,
: the original of a recolour); each must resolve, and its rights are inherited.
  A descriptor checked out of a library derives from its origin by default

title, family, style, origin, status, tags: curation, stored on the record;
: given again with unchanged content they update the record and make no
  version. `style` and `tags` add to what is there, or replace it with
  `replace_curation=True`

note: what changed, stored on the version
expect_head: guard against publishing into an asset you did not mean:

> `None` — the id must be new; `"vNNN"` — the head must be that version

search: further libraries where `derived_from` references resolve (the
: owning library is always searched first)

license_parts: `{glob: source}` — a per-file statement for every stored
: path a glob matches (`match_license_parts()`: `*` stays in one
  folder, `**` crosses folders, case-exact; a glob matching nothing,
  two globs disagreeing on a path, or a near miss refuse), each pinned
  to the file’s digest (an#345). Needs an asset-level `source` (given
  or carried): the files no glob names are stated with the version’s
  label computed WITHOUT these statements. A per-file statement never
  relaxes what the bytes already carry — the per-part source of the same
  file, the same bytes at another path, and every earlier statement of
  this asset’s chain or of a version it derives from about them: a
  looser one is kept and recorded as a rights conflict, the stricter
  binding (an#357), unless `relicense` records who and why (the
  relicence then lists the digests it covers, and a per-file statement
  stricter than it keeps binding). At a later publish a statement is
  carried for its file while the bytes are unchanged; a file changed
  since is recorded `unlabelled`

file_sources: `{path: source}` — the same, by exact stored path (what a
: stored version holds; [`promote()`](_autosummary/an.library.api.html.md#an.library.api.promote) passes it)

strict_assets: refuse a publish that would record a rights conflict
: (`RightsConflict`: a per-part or per-file statement freer than
  what binds its bytes) instead of recording it with a
  `RightsConflictWarning` (an#357: inform, don’t block, by default)

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> doc = {"name": "lamp", "source": {"provider": "me", "license": "cc0-1.0"}}
>>> r = publish(lib, "prop.lamp", doc, {"parts/lamp.svg": b"<svg/>"}, style="reiniger")
>>> str(r.ref), r.created, r.rights.license_class
('an:prop.lamp@v001', True, 'free')
>>> publish(lib, "prop.lamp", doc, {"parts/lamp.svg": b"<svg/>"}).created
False
```

* **Return type:**
  [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)

### an.library.api.publish_dir(library, folder, asset_id, \*\*kwargs)

Publish an asset folder as it sits in a project store (`assets/characters/alice/`).

The descriptor is the kind’s descriptor file (`character.json`); every other
file under the folder is published as one of the asset’s files, so a
check-out reproduces the folder — except operating-system clutter
(`.DS_Store`, hidden files, `Thumbs.db`: `an.stores._common.is_os_junk()`)
that the descriptor does not name — a part it names is published whatever
its file is called (review-288 S2). Keyword arguments go to [`publish()`](_autosummary/an.library.api.html.md#an.library.api.publish).

* **Return type:**
  [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)

### an.library.api.reindex(library, , search=None)

Rebuild `library`’s floor index from its versions. Returns the number of blobs indexed.

Lineage resolves through `search` and then every library on this machine
([`an.library.floor.machine_libraries()`](_autosummary/an.library.floor.html.md#an.library.floor.machine_libraries)), so a promoted copy’s parent
in a genre’s library is read without being named (an#361).

The index is derived data: rebuilding it is always safe, and the way to
repair a library whose index was lost or written by an older `an`. It also
(re-)registers the library’s root in the machine registry
([`an.library.registry`](_autosummary/an.library.registry.html.md#module-an.library.registry)), so a library made at a custom root before the
registry existed becomes visible to every other library’s rights floor.
`reindex_changes()` says first what a rebuild would change.

The new index is computed in full first, then written over the old one
entry by entry, and only then are stale entries removed: a crash midway
leaves old and new statements side by side, never an empty floor (an#249
R4-N4).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### an.library.api.retire(library, asset_id, , by, reason)

Retire an asset id: recorded, hidden from `find` by default, never deleted.

Its versions stay readable — a project pinned to one still checks it out
and validates — and its rights statements still bind the floor (retiring
is curation, not a relabel). `find(status="retired")` lists it; a publish
into it is refused unless it passes `status=` to revive it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.dead", {"name": "dead"})
>>> _ = retire(lib, "prop.dead", by="me", reason="superseded")
>>> len(find(lib)), [h.asset_id for h in find(lib, status="retired")]
(0, ['prop.dead'])
```

### an.library.api.scan_index(library)

Every asset’s head version in `library`, read from the stores.

Affordances snapshotted by an older analyser are recomputed, not trusted. A
record or version that cannot be read (a damaged file) is skipped with a
[`LibraryIndexWarning`](_autosummary/an.library.api.html.md#an.library.api.LibraryIndexWarning) naming it, so one bad entry never blinds every
search.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`IndexEntry`](_autosummary/an.library.api.html.md#an.library.api.IndexEntry)]

### an.library.api.set_status(library, asset_id, status, , by, reason)

Set an asset’s curation status, recording who and why; return the record.

The record’s `status_history` is appended to, never rewritten, and no
version is touched: a project pinned to one keeps reading it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.lamp", {"name": "lamp"})
>>> set_status(lib, "prop.lamp", "approved", by="me", reason="looks right")["status"]
'approved'
```

### an.library.api.show(libraries, ref)

The record, the resolved version, its recomputed rights and the list of versions.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.lamp", {"name": "lamp"}, title="A lamp")
>>> s = show(lib, "prop.lamp")
>>> s["ref"], s["record"]["title"], s["versions"]
('an:prop.lamp@v001', 'A lamp', ['v001'])
```

### an.library.api.unknown_advice(libraries, version, \*, owner=None, floor=<object object>)

What would answer each `unknown` contributor of a version — one sentence per kind.

The advice a refusal prints, so it names what works for THESE gaps
(an#307): a gap of the asset’s own chain takes a relabel; a gap of a
version it derives from is labelled there; another asset’s silence about
bytes this version still holds is answered by labelling THAT asset (a
relabel here cannot speak for another asset), or by dropping the file; and
anything stated (a licence nobody recognises, an unverified stamp, a
parent not on the path) relaxes only by a relicence.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> r = publish(lib, "prop.vase", {"name": "vase"})
>>> r.rights.license_class, "--relabel-by" in r.advice[0]
('unknown', True)
```

### an.library.api.version_labels(library, version)

The labels recorded on a stored version since it was published, oldest first.

A relabel of a version’s UNCHANGED content is recorded on that version, in
the library’s append-only `labels` store, instead of minting a new
version (an#307). A label counts only for the very version it was made on:
its `manifest` must be this version’s (a same-named library’s other
`x@v001` never inherits it). An unreadable label is skipped with a
[`LibraryIndexWarning`](_autosummary/an.library.api.html.md#an.library.api.LibraryIndexWarning); skipping one can only leave the version
stricter.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.library.api.version_sources(libraries, version, \*, floor=<object object>, owner=None, per_file=True, rule=None)

Every labelled source a version’s rights depend on — its own, its lineage, its bytes.

- its own ([`an.library.rights.sources_in()`](_autosummary/an.library.rights.html.md#an.library.rights.sources_in): asset-level, descriptor, parts);
- the version it follows (`previous`) and each `derived_from` version,
  recursively, labelled `<ref> > <label>`. A version records the
  manifest of each parent it resolved at publish (`LINEAGE_FIELD`);
  a parent that no longer resolves to that manifest is “not on the search
  path”, and the rights the child recorded stand in for it;
- the **floor** of every file of every version walked: what any library on
  this machine says about the same bytes ([`an.library.floor`](_autosummary/an.library.floor.html.md#module-an.library.floor)),
  labelled `<path>: same bytes as <asset>@<version>`. A blob is as
  restricted as the strictest statement made about it anywhere.

One exception to reading the floor, so a `relabel` can answer an earlier
version’s gap: the `unknown` statements of a version this walk read in
full AND could verify (each lineage link resolved to its pinned manifest,
or a `previous` link inside the same library root) are not read twice. A
`private` or `attribution` statement is always read, and so is anything
said by a version the walk could not verify (review-269 B1).

floor: a [`BlobFloor`](_autosummary/an.library.floor.html.md#an.library.floor.BlobFloor) to read (default: every
: library on the machine); `None` leaves the floor out — what the
  version itself says (its “asset label”), which is what the floor stores

owner: the library holding `version` (default: unknown — its own
: statements are then read from the floor too, which repeats a reason and
  relaxes nothing)

per_file: count each walked version’s per-file statements
: (`FILE_SOURCES_FIELD`, labelled `file:<path>`; an#345) — they
  count toward a version’s rights, never toward the label that speaks
  for the files nothing itemises (`False`: `_own_label_class()`)

A version carrying an explicit `relicense` (who, why) contributes its
asset-level source alone: that recorded statement replaces everything it
would otherwise inherit, and is the only way to relax rights. A version
carrying a `relabel` (who, why, beside an explicit source) answers the
GAPS of its own `previous` chain with that source — a file recorded
`unlabelled`, a version that recorded no source at all — and nothing else
(`RELABEL_FIELD`). A parent no library on the path holds falls back to
the rights recorded at publish, or `unknown` — never silence.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`AssetSource`](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]]

### an.library.api.vocabulary(libraries, \*, index=<function scan_index>)

Every facet with its values and counts, and the registered capabilities.

What an agent reads to turn words into a typed query (spectrum (b)): “a
Reiniger character who can walk in profile” → `style=reiniger`,
`affords=["limbs.legs", "swap.view:side"]`. The counts are over what
`find` offers by default — the same recomputed rights, and no asset of a
hidden status (`retired`), whose numbers are under `hidden`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> v = vocabulary(lib)
>>> sorted(v)
['capabilities', 'facets', 'hidden', 'kinds', 'rights', 'statuses']
>>> isinstance(v["capabilities"], (dict, list, tuple))
True
```


# _autosummary/an.library.cli.html.md

# an.library.cli

`an library …` — the asset library from the shell, over the same functions as Python.

Wired into the top-level dispatcher as the `library` namespace
(`an.tools._dispatch_namespaces`), programmatically, per pillar 8: these are
plain functions taking strings and booleans and returning the text to print;
the business logic is [`an.library.api`](_autosummary/an.library.api.html.md#module-an.library.api).

Lists are comma-separated (`--style reiniger,gilliam`). Every command reads
the library of `--package` (default `an`) at `--root` (default: the
package’s data folder, or `<PKG>_HOME`); the read commands search that
library, then the core `an` library, then `--extra` ones — and the library
a namespaced reference names (`cutan:character.alice@v002` reads `cutan`
with no `--package`; an#251). A refusal (an unknown asset, a private asset
leaving its library, …) prints one sentence and exits non-zero.

Subcommands: `publish`, `kit`, `find`, `vocabulary`, `show`,
`checkout` (which checks out a whole kit when the reference is one),
`promote`, `retire`.

### Functions

| [`checkout`](_autosummary/an.library.cli.html.md#an.library.cli.checkout)(project_dir, ref[, key, overwrite, ...])   | Check a library version out into a project, and pin it in assets.lock.json.      |
|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`find`](_autosummary/an.library.cli.html.md#an.library.cli.find)([kind, style, affords, rights, family, ...])   | Find assets: AND across facets, OR within one facet's comma-separated values.    |
| [`kit`](_autosummary/an.library.cli.html.md#an.library.cli.kit)(package, asset_id, refs[, key_for, ...])        | Publish a kit: a pinned set of assets a project checks out in one call.          |
| [`promote`](_autosummary/an.library.cli.html.md#an.library.cli.promote)(ref[, package, root, core_root, ...])       | Copy a version into the core an library, so other genres can reuse it.           |
| [`publish`](_autosummary/an.library.cli.html.md#an.library.cli.publish)(folder, asset_id[, package, root, ...])     | Publish an asset folder as the next version of `asset_id`.                       |
| [`retire`](_autosummary/an.library.cli.html.md#an.library.cli.retire)(ref[, by, reason, status, package, root])    | Retire an asset id: hidden from find, never deleted; its versions stay readable. |
| [`show`](_autosummary/an.library.cli.html.md#an.library.cli.show)(ref[, package, root, extra, json_out])         | Show one asset: its record, the resolved version, and its other versions.        |
| [`vocabulary`](_autosummary/an.library.cli.html.md#an.library.cli.vocabulary)([package, root, extra])                  | Every facet value with its count, and every capability with its remedy (JSON).   |

### an.library.cli.checkout(project_dir, ref, key='', overwrite=False, upgrade=False, package='', root='', extra='', only='', skip='')

Check a library version out into a project, and pin it in assets.lock.json.

project_dir: the an project
ref: [<library>:]<asset_id>[@<version>] (latest is resolved now and pinned); a <library>: prefix reads that library, no –package needed. A kit.<slug> reference checks out every member of the kit, each pinned, and records the kit in the lockfile
key: the key in the project store (default: the asset’s slug)
overwrite: replace an existing entry that is not this version (an unedited folder you just published is recognised without it)
upgrade: update in place the entry pinned to an earlier version of this asset, if unedited; the pin moves (then update the scene’s library: line)
package: the library to read first, then the core an library (default: the reference’s <library>: prefix, else an)
root: that library’s root (with no –package, the root of the library the reference names)
extra: further libraries, by package name, comma-separated
only: for a kit, check out only these members (their keys or asset ids, comma-separated)
skip: for a kit, leave these members out (their keys or asset ids, comma-separated)

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.cli.find(kind='', style='', affords='', rights='any', family='', origin='', status='', tags='', near=False, package='an', root='', extra='', json_out=False)

Find assets: AND across facets, OR within one facet’s comma-separated values.

kind: character, prop, environment, …
style: styles, comma-separated (any of them)
affords: capabilities the asset must ALL have, e.g. limbs.legs,swap.view:side
rights: any, publishable, commercial (publishable in a commercial video: not non-commercial), or licence classes (free, attribution, noncommercial, private, unknown)
family: families, comma-separated
origin: origins, comma-separated
status: draft, approved, deprecated, retired (a retired asset is listed only when asked for)
tags: tags, comma-separated (any of them)
near: also list assets that only miss capabilities, with the remedy for each
package: the library to search first (then the core an library)
root: that library’s root
extra: further libraries to search, by package name, comma-separated
json_out: print JSON instead of a table

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.cli.kit(package, asset_id, refs, key_for='', name='', note='', title='', family='', style='', status='', tags='', license='', provider='', author='', source_url='', root='', extra='')

Publish a kit: a pinned set of assets a project checks out in one call.

package: whose library to publish the kit into (an, or a genre such as cutan)
asset_id: kit.<slug>, e.g. kit.reiniger-base
refs: the members, comma-separated [<library>:]<asset_id>[@<version>] (latest is pinned now)
key_for: the project key of a member, as ref=key pairs, comma-separated (default: the asset’s slug)
name: the kit’s name in its document (default: the asset id’s slug)
note: what the kit is for
title: a human title for the record
family: the identity shared across styles and variants
style: styles the kit suits, comma-separated
status: draft, approved, deprecated or retired
tags: free tags, comma-separated
license: licence code of the kit document itself (the members keep their own rights)
provider: where the kit document came from (required with –license)
author: who made it
source_url: where it was fetched from
root: that library’s root (default: the package’s data folder)
extra: further libraries where the members resolve, by package name, comma-separated

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.cli.promote(ref, package='', root='', core_root='', as_id='', allow_restricted=False, strict_assets=False)

Copy a version into the core an library, so other genres can reuse it.

ref: [<library>:]<asset_id>[@<version>]
package: the library it is in (a genre’s, e.g. cutan; default: the reference’s <library>: prefix)
root: that library’s root
core_root: the core an library’s root (default: its data folder)
as_id: promote under another id (when the core library has an unrelated asset with this one)
allow_restricted: copy a private or unknown version anyway (it otherwise never leaves its library)
strict_assets: refuse a copy that would record a rights conflict, instead of recording it

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.cli.publish(folder, asset_id, package='an', root='', title='', family='', style='', origin='', status='', tags='', note='', derived_from='', license='', provider='', author='', source_url='', relicense_by='', relicense_reason='', relabel_by='', relabel_reason='', expect_head='', replace_curation=False, extra='', license_part=None, strict_assets=False)

Publish an asset folder as the next version of `asset_id`.

folder: the asset folder as it sits in a project (e.g. assets/characters/alice)
asset_id: <kind>.<slug>, e.g. character.alice-reiniger
package: whose library to publish into (an, or a genre such as cutan)
root: that library’s root (default: the package’s data folder)
title: a human title for the record
family: the identity shared across styles and variants (e.g. alice)
style: styles the asset suits, comma-separated
origin: drawn, procedural, dicebear, carved, traced, stock, commissioned, generated
status: draft, approved, deprecated or retired (publishing into a retired id needs it, to revive it)
tags: free tags, comma-separated
note: what changed in this version
derived_from: library references this derives from, comma-separated
license: licence code of the asset as a whole (counted BESIDE the descriptor’s own source and everything it inherits; the strictest wins)
provider: where it came from (required with –license)
author: who made it
source_url: where it was fetched from
relicense_by: who relicenses the asset (with –relicense-reason and –license): the only way to relax inherited rights
relicense_reason: why — recorded on the version and shown in its rights
relabel_by: who labels bytes nobody labelled (with –relabel-reason and –license): answers the asset’s earlier unlabelled files and sourceless versions, never a stricter statement nor another asset’s files; on unchanged content it is recorded on the head, no new version
relabel_reason: why — recorded (on the version, or on the head it labels) and shown in its rights
expect_head: refuse unless the asset’s head is this version, or ‘new’ for an id that must not exist yet
replace_curation: –style/–tags replace the record’s lists instead of adding to them
extra: further libraries where –derived-from resolves, by package name, comma-separated
license_part: GLOB=LICENCE[,provider=…,author=…,url=…], repeatable — a licence for the files the glob names (‘\*’ stays in one folder, ‘\*\*’ crosses folders, case-exact); every other file takes the version’s label without them. One looser than what the bytes already carry is kept and reported as a rights conflict, the stricter binding, unless relicensed
strict_assets: refuse a publish that would record a rights conflict (a per-part or per-file licence freer than what binds its bytes) instead of recording it

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.cli.retire(ref, by='', reason='', status='retired', package='', root='')

Retire an asset id: hidden from find, never deleted; its versions stay readable.

ref: [<library>:]<asset_id> — a <library>: prefix names the library (no –package needed)
by: who retires it (recorded)
reason: why (recorded)
status: the status to set instead (draft, approved or deprecated revives or re-curates it)
package: the library it is in (default: the reference’s <library>: prefix, else an)
root: that library’s root

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.cli.show(ref, package='', root='', extra='', json_out=False)

Show one asset: its record, the resolved version, and its other versions.

ref: [<library>:]<asset_id>[@<version>] (latest by default); a <library>: prefix reads that library
package: the library to read first, then the core an library (default: the reference’s <library>: prefix, else an)
root: that library’s root (with no –package, the root of the library the reference names)
extra: further libraries, by package name, comma-separated
json_out: print the full record and version as JSON

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.cli.vocabulary(package='an', root='', extra='')

Every facet value with its count, and every capability with its remedy (JSON).

package: the library to read first (then the core an library)
root: that library’s root
extra: further libraries, by package name, comma-separated

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.library.federation.html.md

# an.library.federation

Libraries federated by a search path: one read view, writes to the owner (plan §1 decision 7).

Each package has its own library root (`an` → `~/.local/share/an`, a genre
such as `cutan` → `~/.local/share/cutan`). A process reads them as one
ordered **search path** — its own library first, then the core `an` library,
then any others the user lists (ADR 0005 decision 1, Harmony’s scope chain
reduced to one mechanism). An id resolves in the first library that has it;
a namespaced id (`cutan:character.alice@v003`) resolves only in the named one.
Writes always go to one owning [`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library), never to the path.

The search path is the seam where a team share, a shipped seed library or a
remote bucket plugs in later: each is just another [`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library) whose mall
was built over different stores.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     path = search_path("cutan", roots={"cutan": f"{d}/cutan", "an": f"{d}/an"})
...     [lib.name for lib in path]
['cutan', 'an']
```

### Module Attributes

| [`Libraries`](_autosummary/an.library.federation.html.md#an.library.federation.Libraries)   | one library, or a search path of them.   |
|--------------------------------------------------------------|------------------------------------------|

### Functions

| [`as_libraries`](_autosummary/an.library.federation.html.md#an.library.federation.as_libraries)(libraries)              | Normalise one library or a search path to a list.                                                                                             |
|---------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| [`open_library`](_autosummary/an.library.federation.html.md#an.library.federation.open_library)([package, root])        | The library of `package`, at `root` (resolved as in [`an.library.root`](_autosummary/an.library.root.html.md#module-an.library.root)). |
| [`resolve`](_autosummary/an.library.federation.html.md#an.library.federation.resolve)(libraries, ref)              | `(library, pinned_ref, version_doc)` for a reference, along the search path.                                                                  |
| [`search_path`](_autosummary/an.library.federation.html.md#an.library.federation.search_path)([package, extra, roots]) | The ordered libraries `package` reads: its own, then the core `an`, then `extra`.                                                             |

### Classes

| [`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library)(name, mall[, root])   | One library: its name (the namespace of its ids), its mall, and its root if on disk.   |
|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|

### Exceptions

| [`AssetNotFoundError`](_autosummary/an.library.federation.html.md#an.library.federation.AssetNotFoundError)   | No library on the search path holds the asset or version asked for.   |
|-----------------------------------------------------------------------|-----------------------------------------------------------------------|

### *exception* an.library.federation.AssetNotFoundError

Bases: [`LookupError`](https://docs.python.org/3/builtins/exceptions.html#LookupError)

No library on the search path holds the asset or version asked for.

### an.library.federation.Libraries

one library, or a search path of them.

* **Type:**
  What the read functions accept

alias of [`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library) | [`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library)]

### *class* an.library.federation.Library(name, mall, root=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One library: its name (the namespace of its ids), its mall, and its root if on disk.

#### *property* blob_rights *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

`sha256 -> {asset: statement}`, derived ([`an.library.floor`](_autosummary/an.library.floor.html.md#module-an.library.floor)).

#### *property* blobs *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

`sha256 -> bytes` (content-addressed).

#### *property* labels *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

statements made
about a version after it was published (an#307).

* **Type:**
  `<asset_id>@<vNNN>/<id> -> label` (write-once)

#### *property* records *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

`asset_id -> record` (mutable curation).

#### *property* versions *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

`<asset_id>@<vNNN> -> version` (write-once).

### an.library.federation.as_libraries(libraries)

Normalise one library or a search path to a list.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library)]

### an.library.federation.open_library(package='an', root=None, \*\*overrides)

The library of `package`, at `root` (resolved as in [`an.library.root`](_autosummary/an.library.root.html.md#module-an.library.root)).

* **Return type:**
  [`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library)

overrides: stores to inject (`records`, `versions`, `blobs`), as in
: [`an.library.stores.build_library_mall()`](_autosummary/an.library.stores.html.md#an.library.stores.build_library_mall)

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> (lib.name, lib.root)
('an', None)
```

### an.library.federation.resolve(libraries, ref)

`(library, pinned_ref, version_doc)` for a reference, along the search path.

A namespaced reference reads only its library; a bare one reads the first
library holding the asset. `latest` resolves to that library’s head and
`sha256:<prefix>` to the one version whose manifest hash starts with it.
The returned reference is pinned (`vNNN`) and namespaced with the library
it resolved in.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library), [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.library.federation.search_path(package='an', , extra=(), roots=None)

The ordered libraries `package` reads: its own, then the core `an`, then `extra`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library)]

extra: further libraries, by package name (resolved like any root) or as
: [`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library) objects (a team share, a seed library)

roots: an explicit root per package name (tests, a non-default layout);
: unnamed packages resolve as usual (`<PKG>_HOME`, then the data folder)

A name appears once, at its first position.


# _autosummary/an.library.floor.html.md

# an.library.floor

The rights floor of a blob: the strictest statement any library on this machine makes about its bytes.

ADR 0005 decision 10’s “most restrictive”, read as the maintainer decided it
(an#236): rights attach to the bytes. A file is as restricted as the strictest
statement ANY library known on this machine makes about its SHA-256 — whichever
library one reads through — and only an explicit, recorded relicence relaxes it.

**What a statement is.** Each version that holds a blob says one thing about it:

- the version’s own per-part `source` for that exact file, if the source pins
  the same digest (`AssetSource.sha256`), never freer than its lineage (see
  below) — a per-part claim about OTHER bytes (a stale factory stamp on a
  re-carved part) itemises nothing;
- otherwise the asset’s own label: its asset-level and descriptor sources and
  its lineage — what the asset says about every file it does not itemise;
- for a relicensed version, its relicence;
- for an itemised file (a per-part source or a per-file statement,
  `file_sources`, an#345), or any file of a version whose lineage carries
  per-file statements: the strictest of those statements (else its label
  computed without per-file statements), the same bytes at its other paths,
  and what its lineage says about these bytes — so a later statement never
  relaxes an earlier one (`an.library.api._PerFileRule`). A freer
  statement is kept and recorded beside the class under `conflicts`
  (an#357: inform, don’t block).

**Where the statements live.** Each library keeps a derived store,
`blob_rights`: `sha256 -> {"<library>:<asset_id>": statement}`, one entry
per asset (its latest version holding the blob), maintained by `publish` and
rebuilt by [`an.library.api.reindex()`](_autosummary/an.library.api.html.md#an.library.api.reindex). Reading a floor is one key lookup per
library, so the cross-library check costs nothing like a scan.

**Which libraries.** Those on the search path, plus every other library on
this machine the floor can find:

- each package root under the current platform data folder
  (`~/.local/share/an`, `~/.local/share/cutan`, …);
- each currently set `<PKG>_HOME` root;
- every root in the machine’s **registry of library roots**
  ([`an.library.registry`](_autosummary/an.library.registry.html.md#module-an.library.registry), an#249) — written on the first write to any
  library, at a path that depends on nothing in the environment. A library at a
  custom `--root`, or under a `<PKG>_HOME` / `XDG_DATA_HOME` that has since
  changed, is therefore still read. A registered root that no longer exists (or
  holds no library) is skipped;
- the machine’s **memory of statements** (same place): every statement any
  on-disk library made at publish, so a library moved, renamed or deleted
  since relaxes nothing — the floor keeps its last-known statements.

A library written by an older `an` at a custom root, before the registry
existed, is registered by its next write or by [`an.library.api.reindex()`](_autosummary/an.library.api.html.md#an.library.api.reindex).

### Functions

| [`library_origin`](_autosummary/an.library.floor.html.md#an.library.floor.library_origin)(library)                           | Which library a statement came from: its resolved root, or this in-memory library.          |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| [`machine_libraries`](_autosummary/an.library.floor.html.md#an.library.floor.machine_libraries)([libraries, environ, platform]) | `libraries` (first, as given) plus every other library root on this machine.                |
| [`record_statement`](_autosummary/an.library.floor.html.md#an.library.floor.record_statement)(store, digest, asset_key, ...)   | Record what `asset_key` says about `digest`: its latest holding version wins.               |
| [`register_library`](_autosummary/an.library.floor.html.md#an.library.floor.register_library)(library)                         | Record an on-disk library's root in the machine registry, before its first write.           |
| [`remember`](_autosummary/an.library.floor.html.md#an.library.floor.remember)(library, statements)                     | Remember an on-disk library's statements in the machine memory (none for an in-memory one). |

### Classes

| [`BlobFloor`](_autosummary/an.library.floor.html.md#an.library.floor.BlobFloor)([libraries, discover])   | The strictest statements about a blob, over every library on the machine, memoised.   |
|-------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|

### *class* an.library.floor.BlobFloor(libraries=None, , discover=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The strictest statements about a blob, over every library on the machine, memoised.

Build one per operation (a publish, a search, a check-out): it opens the
machine’s libraries once and reads each digest once.

#### each(digest)

Every `(origin, asset_key, statement)` about `digest`, unmerged.

`origin` is the library that made it ([`library_origin()`](_autosummary/an.library.floor.html.md#an.library.floor.library_origin)), so a
caller can tell a version it read itself from a same-named library’s.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]]

#### statements(digest, , exclude=())

`{asset_key: statement}` about `digest` from every library.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

exclude: `(origin, manifest)` of versions whose statements to leave
: out — the versions a rights walk reads in full itself
  ([`an.library.api.version_sources()`](_autosummary/an.library.api.html.md#an.library.api.version_sources)), each in the library it
  was read from ([`library_origin()`](_autosummary/an.library.floor.html.md#an.library.floor.library_origin)). A same-named library’s
  version with the same manifest is another version (its lineage
  resolves in ITS library), so it is never left out; and the
  exclusion runs before statements under one asset key are merged,
  so it can hide nothing else.

#### strictest(digests)

The strictest class any statement makes about any of `digests` (`free` if none).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.floor.library_origin(library)

Which library a statement came from: its resolved root, or this in-memory library.

Two libraries can share a name (the default `cutan` and one at a custom
root), and so an asset key, a version label and even a manifest: only the
root tells their statements apart.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.floor.machine_libraries(libraries=None, , environ=None, platform=None)

`libraries` (first, as given) plus every other library root on this machine.

Roots already on the path are not opened twice; a folder whose name is not a
valid library name is skipped.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library)]

### an.library.floor.record_statement(store, digest, asset_key, statement)

Record what `asset_key` says about `digest`: its latest holding version wins.

A later version of an asset may only be as strict or stricter than an
earlier one (it inherits it), unless it is relicensed — so the latest
version’s statement is the asset’s statement.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.library.floor.register_library(library)

Record an on-disk library’s root in the machine registry, before its first write.

An in-memory library (no root) has nothing to register. Raises
[`an.library.registry.RegistryError`](_autosummary/an.library.registry.html.md#an.library.registry.RegistryError) when the registry cannot be
read or written: the write is refused rather than made invisible to the floor.

A registry that does not exist yet means either this machine’s first
library write, or a registry that was deleted (or libraries that predate
it). The libraries on disk tell the two apart (an#307):

- no library discoverable now holds an asset: a **new** registry, created;
- libraries already hold assets: a **lost** one. It is rebuilt from what
  is discoverable now — those libraries are registered, and the statements
  their own floor indexes hold are remembered again — so their rights bind
  exactly as before. What cannot be recovered is a library kept at a
  custom root that is not discoverable now: it binds again once it is
  written to or reindexed.

Either way a `an.library.registry.RegistryWarning` says which
(R2b-N2: a registry deleted wholesale must never pass silently). Never
delete the registry folder wholesale; prune it.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.library.floor.remember(library, statements)

Remember an on-disk library’s statements in the machine memory (none for an in-memory one).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.library.html.md

# an.library

The asset library: reusable assets that outlive their videos (ADR 0005).

A **library** holds characters, props, environments, sounds, styles, voices and
motion clips across videos and styles; a **project** is one video that checks
assets out of it. Each package has its own library root (`an` →
`~/.local/share/an`, a genre such as `cutan` → `~/.local/share/cutan`),
read together as an ordered search path.

The example publishes a character, which is the cut-out genre’s asset (`pip install
"an[cutout]"`), so it is not run where that package is absent:

```pycon
>>> import tempfile
>>> from cutan.characters.schema import CharacterDescriptor
>>> with tempfile.TemporaryDirectory() as d:
...     lib = open_library("an", root=d)
...     doc = CharacterDescriptor(name="blob")
...     r = publish(lib, "character.blob", doc, style="reiniger")
...     [h.ref for h in find(lib, kind="character", affords="swap.view:front")]
['character.blob@v001']
```

What lives where:

- [`an.library.root`](_autosummary/an.library.root.html.md#module-an.library.root) — the root of each package’s data (vendored XDG logic);
- [`an.library.ids`](_autosummary/an.library.ids.html.md#module-an.library.ids) — asset ids, version labels, library references;
- [`an.library.stores`](_autosummary/an.library.stores.html.md#module-an.library.stores) — the mall: `records`, write-once `versions`,
  content-addressed `blobs`, append-only `labels`, all injected
  `MutableMapping` s;
- [`an.library.federation`](_autosummary/an.library.federation.html.md#module-an.library.federation) — [`Library`](_autosummary/an.library.html.md#an.library.Library) and the search path;
- [`an.library.affordances`](_autosummary/an.library.affordances.html.md#module-an.library.affordances) — capabilities and per-kind analysers (the seed
  of ADR 0002’s registry); `an.library.character` — the character analyser;
- [`an.library.rights`](_autosummary/an.library.rights.html.md#module-an.library.rights) — the most-restrictive roll-up over `AssetSource`;
  [`an.library.floor`](_autosummary/an.library.floor.html.md#module-an.library.floor) — the strictest statement any library on the machine
  > makes about a blob; [`an.library.registry`](_autosummary/an.library.registry.html.md#module-an.library.registry) — the machine’s registry of
  > library roots the floor reads, independent of the environment;
- [`an.library.api`](_autosummary/an.library.api.html.md#module-an.library.api) — `publish`, `find`, `vocabulary`, `show`,
  `promote`; [`an.library.checkout`](_autosummary/an.library.html.md#an.library.checkout) — `checkout`, `verify_checkout`,
  `check_pins`, `drift_findings` (both run by `an validate`);
  [`an.library.lock`](_autosummary/an.library.lock.html.md#module-an.library.lock) — the project lockfile (a project store,
  > `mall["library_lock"]`);
- [`an.library.kits`](_autosummary/an.library.kits.html.md#module-an.library.kits) — kits: a curated, pinned set of assets a project checks
  out in one call (`publish_kit`, `checkout_kit`); the kit is recorded in
  the lockfile’s `kits` section;
- [`an.library.cli`](_autosummary/an.library.cli.html.md#module-an.library.cli) — `an library …`, a projection of the same functions.

### Functions

| [`analyse`](_autosummary/an.library.html.md#an.library.analyse)(kind, doc[, art])                          | `(profile, analysers)` of one subject: its capabilities and the analyser versions used.                                                       |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| [`build_library_mall`](_autosummary/an.library.html.md#an.library.build_library_mall)([root, package])                | The library mall of `package`: `records`, `versions` (write-once), `blobs` (CAS).                                                             |
| [`check_pins`](_autosummary/an.library.html.md#an.library.check_pins)(scene, lock)                            | Findings where a scene's `AssetRef.library` and the project lockfile disagree.                                                                |
| [`checkout`](_autosummary/an.library.html.md#an.library.checkout)(libraries, project_dir, ref, \*[, ...])   | Materialise a library version into a project, carry its rights, pin it.                                                                       |
| [`checkout_kit`](_autosummary/an.library.html.md#an.library.checkout_kit)(libraries, project_dir, ref, \*)      | Check the members of a kit out into a project, pin each, and record the kit.                                                                  |
| [`drift_findings`](_autosummary/an.library.html.md#an.library.drift_findings)([project_dir, mall, lock, ...])     | One `info` Finding per checked-out entry that is no longer — or cannot be shown to be — its pinned version.                                   |
| [`effective_rights`](_autosummary/an.library.html.md#an.library.effective_rights)(libraries, version, \*[, ...])    | The rights of a version, recomputed from its sources, its lineage and its bytes.                                                              |
| [`find`](_autosummary/an.library.html.md#an.library.find)(libraries, \*[, kind, style, affords, ...])   | Assets matching every facet given (AND across facets, OR within one facet's values).                                                          |
| [`library_root`](_autosummary/an.library.html.md#an.library.library_root)([root, package, environ, platform])   | The data root of `package` — its library and its projects live under it.                                                                      |
| [`open_library`](_autosummary/an.library.html.md#an.library.open_library)([package, root])                      | The library of `package`, at `root` (resolved as in [`an.library.root`](_autosummary/an.library.root.html.md#module-an.library.root)). |
| [`parse_ref`](_autosummary/an.library.html.md#an.library.parse_ref)(text, \*[, require_version, ...])        | Parse a library reference.                                                                                                                    |
| [`project_dir`](_autosummary/an.library.html.md#an.library.project_dir)(project_id[, root, package, environ])  | The default directory of the agent-made project `project_id` (design §7.5).                                                                   |
| [`projects_root`](_autosummary/an.library.html.md#an.library.projects_root)([root, package, environ])            | Where agent-made projects go by default: `<root>/projects/`.                                                                                  |
| [`promote`](_autosummary/an.library.html.md#an.library.promote)(libraries, ref, \*[, to, as_id, ...])      | Copy one version into another library — by default the core `an` library.                                                                     |
| [`publish`](_autosummary/an.library.html.md#an.library.publish)(library, asset_id, doc[, files, ...])      | Publish `doc` and its `files` as the next version of `asset_id` in `library`.                                                                 |
| [`publish_dir`](_autosummary/an.library.html.md#an.library.publish_dir)(library, folder, asset_id, \*\*kwargs) | Publish an asset folder as it sits in a project store (`assets/characters/alice/`).                                                           |
| [`publish_kit`](_autosummary/an.library.html.md#an.library.publish_kit)(library, asset_id, members, \*[, ...]) | Publish a kit — a pinned set of assets — as the next version of `asset_id` in `library`.                                                      |
| [`reindex`](_autosummary/an.library.html.md#an.library.reindex)(library, \*[, search])                     | Rebuild `library`'s floor index from its versions.                                                                                            |
| [`retire`](_autosummary/an.library.html.md#an.library.retire)(library, asset_id, \*, by, reason)          | Retire an asset id: recorded, hidden from `find` by default, never deleted.                                                                   |
| [`register_analyser`](_autosummary/an.library.html.md#an.library.register_analyser)(kind, \*[, version, ...])        | Register an analyser.                                                                                                                         |
| [`register_asset_kind`](_autosummary/an.library.html.md#an.library.register_asset_kind)(name, \*[, store, ...])        | Register (or re-register) an asset kind.                                                                                                      |
| [`register_capability`](_autosummary/an.library.html.md#an.library.register_capability)(name, \*[, description, ...])  | Register a capability (or a [`Capability`](_autosummary/an.library.html.md#an.library.Capability)).                                                     |
| [`register_root`](_autosummary/an.library.html.md#an.library.register_root)(package, root, \*[, registry])       | Record `root` (`package`'s library root) in the registry; `True` if it was new.                                                               |
| [`registered_roots`](_autosummary/an.library.html.md#an.library.registered_roots)(\*[, registry])                   | `(package, root)` for every root ever registered, oldest first, each once.                                                                    |
| [`resolve`](_autosummary/an.library.html.md#an.library.resolve)(libraries, ref)                            | `(library, pinned_ref, version_doc)` for a reference, along the search path.                                                                  |
| [`roll_up`](_autosummary/an.library.html.md#an.library.roll_up)(sources, \*[, inherited])                  | Roll labelled sources (and parents' rights) up to one [`Rights`](_autosummary/an.library.html.md#an.library.Rights).                                |
| [`scan_index`](_autosummary/an.library.html.md#an.library.scan_index)(library)                                | Every asset's head version in `library`, read from the stores.                                                                                |
| [`search_path`](_autosummary/an.library.html.md#an.library.search_path)([package, extra, roots])               | The ordered libraries `package` reads: its own, then the core `an`, then `extra`.                                                             |
| [`set_status`](_autosummary/an.library.html.md#an.library.set_status)(library, asset_id, status, \*, by, ...) | Set an asset's curation status, recording who and why; return the record.                                                                     |
| [`sheet`](_autosummary/an.library.html.md#an.library.sheet)(refs, \*[, libraries, package, out, ...])    | Write a contact sheet of `refs` (`[<library>:]<id>[@<version>]`) to one PNG.                                                                  |
| [`show`](_autosummary/an.library.html.md#an.library.show)(libraries, ref)                               | The record, the resolved version, its recomputed rights and the list of versions.                                                             |
| [`verify_checkout`](_autosummary/an.library.html.md#an.library.verify_checkout)(libraries, project_dir, \*[, ...]) | `{<store>/<key>: differences}` for every pinned entry; empty lists are intact copies.                                                         |
| [`version_labels`](_autosummary/an.library.html.md#an.library.version_labels)(library, version)                   | The labels recorded on a stored version since it was published, oldest first.                                                                 |
| [`vocabulary`](_autosummary/an.library.html.md#an.library.vocabulary)(libraries, \*[, index])                 | Every facet with its values and counts, and the registered capabilities.                                                                      |

### Classes

| [`Capability`](_autosummary/an.library.html.md#an.library.Capability)(name, description, remedy[, ...])      | A registered capability: its name, what it means, and how to add it.                 |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`CheckoutResult`](_autosummary/an.library.html.md#an.library.CheckoutResult)(ref, store, key, ...)              | Where a checked-out version landed in the project, and its pin.                      |
| [`FindResult`](_autosummary/an.library.html.md#an.library.FindResult)(hits, near, counts)                    | Hits, near misses (with `near=True`), and per-facet value counts over the hits.      |
| [`Hit`](_autosummary/an.library.html.md#an.library.Hit)(library, asset_id, version, score[, ...])     | One asset that answers a query — or nearly does (`missing` non-empty).               |
| [`Kit`](_autosummary/an.library.html.md#an.library.Kit)(\*\*data)                                     | The kit document: a name, its pinned members, and a note saying what it is for.      |
| [`KitMember`](_autosummary/an.library.html.md#an.library.KitMember)(\*\*data)                               | One member of a kit: a pinned reference and the project key it lands under.          |
| [`Library`](_autosummary/an.library.html.md#an.library.Library)(name, mall[, root])                       | One library: its name (the namespace of its ids), its mall, and its root if on disk. |
| [`LibraryRef`](_autosummary/an.library.html.md#an.library.LibraryRef)(asset_id[, version, namespace])        | A parsed `[<namespace>:]<asset_id>[@<version>]`.                                     |
| [`ProjectLock`](_autosummary/an.library.html.md#an.library.ProjectLock)(project_dir)                          | `<store>/<key> -> pin` over a project's `assets.lock.json`.                          |
| [`PublishResult`](_autosummary/an.library.html.md#an.library.PublishResult)(ref, manifest_sha256, created, ...) | What a publish did: the version it names, and whether it made one.                   |
| [`Rights`](_autosummary/an.library.html.md#an.library.Rights)(license_class[, reasons])                  | The rolled-up rights of one version, as stored on it.                                |

### Exceptions

| [`AssetIdError`](_autosummary/an.library.html.md#an.library.AssetIdError)       | An asset id, version label or library reference that does not parse.                    |
|---------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| [`AssetNotFoundError`](_autosummary/an.library.html.md#an.library.AssetNotFoundError) | No library on the search path holds the asset or version asked for.                     |
| [`CheckoutError`](_autosummary/an.library.html.md#an.library.CheckoutError)      | A version cannot be materialised into this project as asked.                            |
| [`IntegrityError`](_autosummary/an.library.html.md#an.library.IntegrityError)     | Stored bytes, paths or a stored manifest do not match what was recorded.                |
| [`LibraryError`](_autosummary/an.library.html.md#an.library.LibraryError)       | A library operation refused, with a sentence saying why and what to do.                 |
| [`RegistryError`](_autosummary/an.library.html.md#an.library.RegistryError)      | The machine's registry or statement memory cannot be read or written; nothing proceeds. |
| [`RightsRefusal`](_autosummary/an.library.html.md#an.library.RightsRefusal)      | A private or unknown version would leave the user's library without an override.        |
| [`VersionExistsError`](_autosummary/an.library.html.md#an.library.VersionExistsError) | A write-once key was written twice, or deleted.                                         |

### *exception* an.library.AssetIdError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

An asset id, version label or library reference that does not parse.

### *exception* an.library.AssetNotFoundError

Bases: [`LookupError`](https://docs.python.org/3/builtins/exceptions.html#LookupError)

No library on the search path holds the asset or version asked for.

### *class* an.library.Capability(name, description, remedy, subject='asset', command=None, version='1')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A registered capability: its name, what it means, and how to add it.

`command` is the CLI that adds it, when one exists (`an character
add-views`); `version` bumps when the *meaning* of the name changes (a
persisted name is never redefined in place).

#### to_json()

The capability as the generated docs and the MCP surface list it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *exception* an.library.CheckoutError

Bases: [`LibraryError`](_autosummary/an.library.api.html.md#an.library.api.LibraryError)

A version cannot be materialised into this project as asked.

### *class* an.library.CheckoutResult(ref, store, key, manifest_sha256, files, changed, rights)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where a checked-out version landed in the project, and its pin.

#### asset_ref(entity_id=None)

An [`AssetRef`](_autosummary/an.ir.schema.html.md#an.ir.schema.AssetRef) casting this asset, pinned by `library`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### *class* an.library.FindResult(hits, near, counts)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Hits, near misses (with `near=True`), and per-facet value counts over the hits.

#### to_dict()

A JSON-ready view.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* an.library.Hit(library, asset_id, version, score, title=None, license_class='unknown', missing=<factory>, remedies=<factory>, federated=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One asset that answers a query — or nearly does (`missing` non-empty).

#### *property* ref *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

namespaced when the search spanned several libraries.

* **Type:**
  The reference to pin

#### to_dict()

A JSON-ready view (CLI `--json`, MCP).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *exception* an.library.IntegrityError

Bases: [`LibraryError`](_autosummary/an.library.api.html.md#an.library.api.LibraryError)

Stored bytes, paths or a stored manifest do not match what was recorded.

### *class* an.library.Kit(\*\*data)

Bases: `BaseModel`

The kit document: a name, its pinned members, and a note saying what it is for.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.library.KitMember(\*\*data)

Bases: `BaseModel`

One member of a kit: a pinned reference and the project key it lands under.

`key=None` takes the check-out’s default (the asset id’s slug).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.library.Library(name, mall, root=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One library: its name (the namespace of its ids), its mall, and its root if on disk.

#### *property* blob_rights *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

`sha256 -> {asset: statement}`, derived ([`an.library.floor`](_autosummary/an.library.floor.html.md#module-an.library.floor)).

#### *property* blobs *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

`sha256 -> bytes` (content-addressed).

#### *property* labels *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

statements made
about a version after it was published (an#307).

* **Type:**
  `<asset_id>@<vNNN>/<id> -> label` (write-once)

#### *property* records *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

`asset_id -> record` (mutable curation).

#### *property* versions *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)*

`<asset_id>@<vNNN> -> version` (write-once).

### *exception* an.library.LibraryError

Bases: [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)

A library operation refused, with a sentence saying why and what to do.

### *class* an.library.LibraryRef(asset_id, version=None, namespace=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A parsed `[<namespace>:]<asset_id>[@<version>]`.

`version` is `None` when the reference gave none (read as `latest` by
readers; refused where a pin is required, e.g. `AssetRef.library`).

#### *property* is_pinned *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether this names one immutable version (a label or a content hash).

#### *property* kind *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The asset kind, from the id’s prefix.

#### with_namespace(namespace)

The same asset and version, qualified by another library name.

* **Return type:**
  [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef)

#### with_version(version)

The same asset, another version.

* **Return type:**
  [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef)

### *class* an.library.ProjectLock(project_dir)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

`<store>/<key> -> pin` over a project’s `assets.lock.json`.

Every write rewrites the whole (small) file, sorted, so the lockfile diffs
cleanly under version control. The mapping is the `assets` section;
[`kits`](_autosummary/an.library.kits.html.md#module-an.library.kits) is the `kits` section, and a write to either keeps the other.

#### *property* kits *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]*

`kit asset id -> record` of the kits checked out into the project.

### *class* an.library.PublishResult(ref, manifest_sha256, created, rights, affordances, advice=(), conflicts=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a publish did: the version it names, and whether it made one.

#### advice *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ()*

what would answer each kind of gap, one
sentence each (`unknown_advice()`).

* **Type:**
  For an `unknown` result

#### conflicts *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[RightsConflict, ...]* *= ()*

statements freer
than what binds their bytes, kept beside the stricter one that binds.

* **Type:**
  The rights conflicts the version records (an#357)

### *exception* an.library.RegistryError

Bases: [`OSError`](https://docs.python.org/3/builtins/exceptions.html#OSError)

The machine’s registry or statement memory cannot be read or written; nothing proceeds.

### *class* an.library.Rights(license_class, reasons=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The rolled-up rights of one version, as stored on it.

#### *property* commercial *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether a COMMERCIAL video containing this asset may ship (an#373).

#### *classmethod* from_dict(d)

Read a version’s `rights` block back.

* **Return type:**
  [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights)

#### *property* publishable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether a video containing this asset may ship (commercially or not).

#### to_dict()

The `rights` block of a version document.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *exception* an.library.RightsRefusal

Bases: [`PermissionError`](https://docs.python.org/3/builtins/exceptions.html#PermissionError)

A private or unknown version would leave the user’s library without an override.

### *exception* an.library.VersionExistsError

Bases: [`KeyError`](https://docs.python.org/3/builtins/exceptions.html#KeyError)

A write-once key was written twice, or deleted. Versions are immutable.

### an.library.analyse(kind, doc, art=None)

`(profile, analysers)` of one subject: its capabilities and the analyser versions used.

A kind with no registered analyser affords nothing *derived* and records no
analyser — an honest empty answer, not a guess.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> analyse("no-such-kind", {})
({}, {})
```

### an.library.build_library_mall(root=None, , package='an', \*\*overrides)

The library mall of `package`: `records`, `versions` (write-once), `blobs` (CAS).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)]

root: the package’s data root (default: resolved by
: [`an.library.root.library_root()`](_autosummary/an.library.root.html.md#an.library.root.library_root) — `root` → `<PKG>_HOME` → the
  platform data folder); the stores live under `<root>/library/`

package: whose library this is (`an`, or a genre such as `cutan`)
overrides: a store per name to inject instead of the folder default — a

> `dict` for tests, an S3 or database mapping later. An injected
> `versions` is still made write-once and an injected `blobs` still
> content-addressed and undeletable, so injection cannot drop an invariant.

Nothing is created until the first write: opening a library (or mistyping
one on a search path) leaves the disk as it was.

### an.library.check_pins(scene, lock)

Findings where a scene’s `AssetRef.library` and the project lockfile disagree.

The lockfile is the source of truth (it is what the check-out wrote, beside
the files); the scene’s `library:` restates it. Two records of one pin can
drift — a re-check-out updates the lockfile, not the scene — so `an
validate` runs this on every project (an#240). Each disagreement, and each
`library:` the lockfile does not pin, is a `warning`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.library.checkout(libraries, project_dir, ref, , key=None, mall=None, lock=None, overwrite=False, upgrade=False, for_kit=False)

Materialise a library version into a project, carry its rights, pin it.

* **Return type:**
  [`CheckoutResult`](_autosummary/an.library.html.md#an.library.CheckoutResult)

project_dir: the project to check out into
ref: `[<library>:]<asset_id>[@<version>]`; `latest` (or no version) is

> resolved now and pinned

key: the key in the project store (default: the asset id’s slug)
mall: the project mall (default: `build_project_mall(project_dir)`)
lock: the lockfile mapping (default: the mall’s `library_lock` store,

> `<project_dir>/assets.lock.json`)

overwrite: replace an existing entry that is not exactly this version — a
: local fork (any edited file or descriptor) or another asset; without it
  that is refused

upgrade: update in place an entry pinned to an EARLIER version of this
: very asset (by lineage) and unedited since: only the files the earlier
  version named are replaced, and the pin moves (an#346). Without it such
  an entry refuses, naming this flag — never a copy beside it; an edited
  one refuses either way (publish it first, or `overwrite`)

for_kit: the check-out of a kit member (its refusals do not name `key`)

An entry that already IS this version byte for byte — the folder a
`publish` just sent to the library, still unedited — is recognised and
linked (origin block, carried source, pin) without `overwrite`: publishing
a project’s asset and checking it back out is the natural first round trip.

Every stored path, blob and the manifest are verified before anything is
written, and every file is written inside the entry’s folder or not at all.
Editing the checked-out copy forks it; `publish` of the edited folder
sends it back as a new version derived from this one.

### an.library.checkout_kit(libraries, project_dir, ref, , overwrite=False, upgrade=False, only=None, skip=(), mall=None, lock=None)

Check the members of a kit out into a project, pin each, and record the kit.

libraries: where the kit and its members resolve
project_dir: the project to check out into
ref: `[<library>:]<kit asset id>[@<version>]`; `latest` is resolved now
overwrite: replace project entries that are not exactly their member’s version
upgrade: update in place a member’s entry pinned to an earlier version of

> that member, unedited since (`checkout()`)

only: check out only these members, each named by the key it lands under
: (`narrator`) or by its asset id (`voice.narrator`); default: all

skip: leave these members out, named the same way
mall: the project mall (default: `build_project_mall(project_dir)`)
lock: the lockfile (default: the mall’s `library_lock` store); it needs a

> `kits` section, as [`ProjectLock`](_autosummary/an.stores.library_lock.html.md#an.stores.library_lock.ProjectLock) has

Returns one [`CheckoutResult`](_autosummary/an.library.html.md#an.library.CheckoutResult) per member checked
out, in the kit’s order. Each member is checked out by `checkout()`
under its `key` and pinned in `assets.lock.json` as any check-out is; the
kit itself is recorded under the lockfile’s `kits` section (its pinned
reference, manifest and the members’ lockfile keys), so `an library` readers
and a human can see which kit the project came from. Checking the same kit out
again is idempotent. A partial check-out (`only`, `skip`, an#400) records
the members the project holds: a later check-out of more members of the same
kit version adds to them.

All members are resolved and checked before the first is written: a missing
member, a member that is itself a kit, a corrupt stored file, or a project
entry that is a fork (without `overwrite`) raises [`CheckoutError`](_autosummary/an.library.html.md#an.library.CheckoutError)
and the project is untouched.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`CheckoutResult`](_autosummary/an.library.html.md#an.library.CheckoutResult)]

### an.library.drift_findings(project_dir=None, , mall=None, lock=None, libraries=None)

One `info` Finding per checked-out entry that is no longer — or cannot be shown to be — its pinned version.

An edited check-out is a fork, not a mistake — hence `info`: it says the
pin now records where the copy CAME FROM, not what it IS, so nothing may
treat the pin as standing for the content (an#240), and publishing the
folder would make a new version. A pin this machine cannot check (its
library is at a custom root, or elsewhere) is its own `info`, with no
advice to check anything out again — that could swap the asset for a
same-named other one.

project_dir: the project (not needed when both `mall` and `lock` are given)

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.library.effective_rights(libraries, version, \*, floor=<object object>, owner=None, rule=None)

The rights of a version, recomputed from its sources, its lineage and its bytes.

owner: the library holding `version` (see `version_sources()`)
rule: a `_PerFileRule` to share with the caller (one per operation)

* **Return type:**
  [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights)

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.vase", {"name": "vase"},
...             source={"provider": "film", "license": "all-rights-reserved"})
>>> effective_rights(lib, read_version(lib, "prop.vase", "v001"), owner=lib).license_class
'private'
```

### an.library.find(libraries, \*, kind=None, style=None, affords=None, rights='any', family=None, origin=None, status=None, tags=None, art=None, near=False, index=<function scan_index>)

Assets matching every facet given (AND across facets, OR within one facet’s values).

* **Return type:**
  [`FindResult`](_autosummary/an.library.api.html.md#an.library.api.FindResult)

affords: capabilities the asset must ALL have — `limbs.legs`, or
: `swap.view:side` for a capability with a given key. Each capability is
  its own boolean facet, so a list of them is AND, as across facets. An
  unregistered name raises, naming the close ones

rights: `any` (default — study renders are legitimate), `publishable`
: (`free` + `attribution` + `noncommercial`), `commercial`
  (`free` + `attribution`: what a commercial video may use, an#373),
  or licence classes. Rights are recomputed
  from each version’s sources and lineage, not read from its cache

status: curation statuses; an asset whose status is hidden
: (`HIDDEN_STATUSES`: `retired`) is offered only when its status
  is asked for by name

near: also return assets that pass every other facet but miss some
: capabilities, or are curated for another style than asked, each with
  what is missing and the remedy that would add it (a style mismatch is
  listed as `style:<wanted>`, remedied by restyling: an#271)

index: the index to read (default: a scan of the stores)

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.lamp", {"name": "lamp"}, style=["reiniger", "gilliam"])
>>> [h.ref for h in find(lib, kind="prop", style="gilliam")]
['prop.lamp@v001']
>>> len(find(lib, kind="prop", rights="publishable"))  # no source: unknown
0
```

### an.library.library_root(root=None, , package='an', environ=None, platform=None)

The data root of `package` — its library and its projects live under it.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

root: an explicit root; wins over everything
package: the package whose root this is (`an` for the core library, a

> genre’s own name for its library)

environ: the environment to read (default `os.environ`; injectable for tests)
platform: `sys.platform` value to resolve for (default: this one)

```pycon
>>> library_root("~/x", package="an").name
'x'
```

### an.library.open_library(package='an', root=None, \*\*overrides)

The library of `package`, at `root` (resolved as in [`an.library.root`](_autosummary/an.library.root.html.md#module-an.library.root)).

* **Return type:**
  [`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library)

overrides: stores to inject (`records`, `versions`, `blobs`), as in
: [`an.library.stores.build_library_mall()`](_autosummary/an.library.stores.html.md#an.library.stores.build_library_mall)

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> (lib.name, lib.root)
('an', None)
```

### an.library.parse_ref(text, , require_version=False, require_pin=False)

Parse a library reference.

`require_version=True` refuses a reference with no `@<version>`;
`require_pin=True` also refuses `@latest` — the grammar of
`AssetRef.library`, where a floating reference would make a render depend
on whatever the library’s head is that day.

* **Return type:**
  [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef)

```pycon
>>> parse_ref("character.alice")
LibraryRef(asset_id='character.alice', version=None, namespace=None)
>>> parse_ref("character.alice@sha256:0a1b2c3d").version
'sha256:0a1b2c3d'
>>> parse_ref("character.alice", require_version=True)
Traceback (most recent call last):
AssetIdError: ...
```

### an.library.project_dir(project_id, root=None, , package='an', environ=None)

The default directory of the agent-made project `project_id` (design §7.5).

`an init <dir>` with an explicit directory keeps working anywhere; this is
the default for projects an agent makes (`an init --id <id>`), so they
never land in a session’s working folder or a repository. `package` is
the library package of the genre the video is made in
([`an.genres.genre_library()`](_autosummary/an.genres.html.md#an.genres.genre_library); the core’s, `an`, by default — the core
names no genre).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> project_dir("alice-and-bob", "/lib").as_posix()
'/lib/projects/alice-and-bob'
>>> project_dir("../escape", "/lib")
Traceback (most recent call last):
ValueError: ...
```

### an.library.projects_root(root=None, , package='an', environ=None)

Where agent-made projects go by default: `<root>/projects/`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> projects_root("/lib", package="cutan").as_posix()
'/lib/projects'
```

### an.library.promote(libraries, ref, , to=None, as_id=None, allow_restricted=False, strict_assets=False)

Copy one version into another library — by default the core `an` library.

An asset made in a genre’s library and reused across genres is promoted to the
core (plan §1 decision 7). The copy is a new version in the target, derived
from the source version, with the record’s curation carried over.

- A `private` or `unknown` version is refused unless
  `allow_restricted=True`: private-study material never leaves its library
  by default (ADR 0005 decision 10). The rights checked are recomputed from
  the version’s sources and lineage, and the stricter of those and the
  stored ones wins.
- If the target already has an asset with this id that does not derive from
  the one promoted (another character that happens to share the id), it is
  refused: promoting would make it a new version of an unrelated asset.
  `as_id` promotes under another id.
- `strict_assets`: refuse a copy that would record a rights conflict
  ([`publish()`](_autosummary/an.library.html.md#an.library.publish)’s, an#357), instead of recording it.

* **Return type:**
  [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)

### an.library.publish(library, asset_id, doc, files=None, \*, source=None, relicense=None, relabel=None, derived_from=(), title=None, family=None, style=None, origin=None, status=None, tags=None, replace_curation=False, note=None, expect_head=<object object>, search=None, carry_source=True, license_parts=None, file_sources=None, strict_assets=False)

Publish `doc` and its `files` as the next version of `asset_id` in `library`.

library: the owning library (writes never go to a search path)
asset_id: `<kind>.<slug>`; the kind must be registered
doc: the descriptor — the existing document `an` already versions

> (`CharacterDescriptor`, …), as a model or its JSON dict, stored verbatim

files: the asset’s files, by the relative paths the descriptor uses
: (`parts/head.svg`) — stored once each in the content-addressed blobs

source: provenance declared for the asset as a whole. It contributes BESIDE
: the descriptor’s own `source` (the most restrictive wins), never
  instead of it. With no `source`, and a descriptor declaring nothing
  or exactly what the head’s declared, the source of the previous version
  carries forward (`carry_source`) — for the bytes it was declared on
  only: a file changed or added since is recorded as `unlabelled` on the
  version and is `unknown`. Later versions inherit that gap through
  `previous` even when they pass `source=`; `relabel` (or a
  relicence) answers it. With no source at all the version is `unknown`
  — recorded and visible, not refused

relicense: `{"by": who, "reason": why}` — the ONLY way to relax rights.
: Rights attach to the bytes and the lineage: a new version inherits the
  version it follows (`previous`), every version it derives from, and
  every version holding the same file bytes, and may only be more
  restrictive than they are — a cc0 in the descriptor or in `source=`
  never relabels private art. A relicence makes `source` (required) the
  whole statement, records who and why on the version (and in its
  manifest and reasons), and covers these bytes for later versions

relabel: `{"by": who, "reason": why}` beside an explicit `source=`
: (required) — a first statement about bytes NOBODY labelled (an#263):
  the gaps of this asset’s own version chain (files an earlier version
  recorded `unlabelled`, files no person’s source spoke for, an earlier
  version with no source at all) are answered with `source`, and so is
  another asset’s silence about a file the chain held once and this
  version no longer holds (an#307). It relaxes no statement anyone made:
  a private (or any) licence, a per-part source, a version this one
  derives from, and every other asset’s statement about bytes this
  version holds still bind. On content that changed, recorded on the new
  version, in its manifest and in its reasons; on UNCHANGED content,
  recorded on the head in the append-only `labels` store
  ([`version_labels()`](_autosummary/an.library.html.md#an.library.version_labels)) and no version is minted (an#307)

derived_from: library references this version derives from (an earlier version,
: the original of a recolour); each must resolve, and its rights are inherited.
  A descriptor checked out of a library derives from its origin by default

title, family, style, origin, status, tags: curation, stored on the record;
: given again with unchanged content they update the record and make no
  version. `style` and `tags` add to what is there, or replace it with
  `replace_curation=True`

note: what changed, stored on the version
expect_head: guard against publishing into an asset you did not mean:

> `None` — the id must be new; `"vNNN"` — the head must be that version

search: further libraries where `derived_from` references resolve (the
: owning library is always searched first)

license_parts: `{glob: source}` — a per-file statement for every stored
: path a glob matches (`match_license_parts()`: `*` stays in one
  folder, `**` crosses folders, case-exact; a glob matching nothing,
  two globs disagreeing on a path, or a near miss refuse), each pinned
  to the file’s digest (an#345). Needs an asset-level `source` (given
  or carried): the files no glob names are stated with the version’s
  label computed WITHOUT these statements. A per-file statement never
  relaxes what the bytes already carry — the per-part source of the same
  file, the same bytes at another path, and every earlier statement of
  this asset’s chain or of a version it derives from about them: a
  looser one is kept and recorded as a rights conflict, the stricter
  binding (an#357), unless `relicense` records who and why (the
  relicence then lists the digests it covers, and a per-file statement
  stricter than it keeps binding). At a later publish a statement is
  carried for its file while the bytes are unchanged; a file changed
  since is recorded `unlabelled`

file_sources: `{path: source}` — the same, by exact stored path (what a
: stored version holds; [`promote()`](_autosummary/an.library.html.md#an.library.promote) passes it)

strict_assets: refuse a publish that would record a rights conflict
: (`RightsConflict`: a per-part or per-file statement freer than
  what binds its bytes) instead of recording it with a
  `RightsConflictWarning` (an#357: inform, don’t block, by default)

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> doc = {"name": "lamp", "source": {"provider": "me", "license": "cc0-1.0"}}
>>> r = publish(lib, "prop.lamp", doc, {"parts/lamp.svg": b"<svg/>"}, style="reiniger")
>>> str(r.ref), r.created, r.rights.license_class
('an:prop.lamp@v001', True, 'free')
>>> publish(lib, "prop.lamp", doc, {"parts/lamp.svg": b"<svg/>"}).created
False
```

* **Return type:**
  [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)

### an.library.publish_dir(library, folder, asset_id, \*\*kwargs)

Publish an asset folder as it sits in a project store (`assets/characters/alice/`).

The descriptor is the kind’s descriptor file (`character.json`); every other
file under the folder is published as one of the asset’s files, so a
check-out reproduces the folder — except operating-system clutter
(`.DS_Store`, hidden files, `Thumbs.db`: `an.stores._common.is_os_junk()`)
that the descriptor does not name — a part it names is published whatever
its file is called (review-288 S2). Keyword arguments go to [`publish()`](_autosummary/an.library.html.md#an.library.publish).

* **Return type:**
  [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)

### an.library.publish_kit(library, asset_id, members, , search=None, name=None, note=None, \*\*curation)

Publish a kit — a pinned set of assets — as the next version of `asset_id` in `library`.

library: the owning library (writes never go to a search path)
asset_id: `kit.<slug>`
members: each a library reference (`[<library>:]<asset_id>[@<version>]`), a

> `(reference, key)` pair, or a mapping `{"ref": …, "key": …}`; `key` is
> the name the member takes in the project’s store (default: the asset’s slug)

search: further libraries where member references resolve (the owning library
: is always searched first, as in [`publish()`](_autosummary/an.library.api.html.md#an.library.api.publish))

name: the kit’s name in its document (default: the asset id’s slug)
note: what the kit is for, stored in the document and on the version
curation: everything else [`publish()`](_autosummary/an.library.api.html.md#an.library.api.publish) takes — `source=`

> (the kit document is its author’s own authoring: its rights come from
> this, never from its members), `title`, `style`, `tags`, …

Every member must resolve; `latest` and unversioned references are pinned to
the concrete version now, so the kit version is reproducible. A member that is
itself a kit, one with no project store, or two members landing in one
`(store, key)` are refused.

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "style.noir", {"name": "noir"}, source={"provider": "me", "license": "cc0-1.0"})
>>> r = publish_kit(lib, "kit.noir-base", ["style.noir"], source={"provider": "me", "license": "cc0-1.0"})
>>> lib.versions["kit.noir-base@v001"]["doc"]["members"]
[{'ref': 'style.noir@v001', 'key': None}]
```

* **Return type:**
  [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)

### an.library.register_analyser(kind, , version='', subject='asset', owner='an')

Register an analyser. Two forms.

`register_analyser(Analyser(...), owner=...)` registers the object and
returns it (a genre’s `analysers` field goes this way). With a `kind`
string it is a decorator: `@register_analyser("character", version="0.1.0")`
registers the decorated derivation.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.library.register_asset_kind(name, , store=None, descriptor=None, credits_store=None)

Register (or re-register) an asset kind. Returns it.

* **Return type:**
  [`AssetKind`](_autosummary/an.library.kinds.html.md#an.library.kinds.AssetKind)

### an.library.register_capability(name, , description='', remedy='', subject='asset', command=None, version='1', owner='an')

Register a capability (or a [`Capability`](_autosummary/an.library.html.md#an.library.Capability)). Returns it.

Re-registering the same definition is a no-op; a different definition
under a name another owner holds raises, because capability names are
persisted and two meanings for one name would make a stored facet lie.

* **Return type:**
  [`Capability`](_autosummary/an.capabilities.html.md#an.capabilities.Capability)

```pycon
>>> cap = register_capability("demo.thing", description="a thing", remedy="add one", owner="demo")
>>> CAPABILITIES["demo.thing"] is cap
True
>>> _ = drop_owner("demo")
```

### an.library.register_root(package, root, , registry=None)

Record `root` (`package`’s library root) in the registry; `True` if it was new.

Idempotent. Raises [`RegistryError`](_autosummary/an.library.html.md#an.library.RegistryError) when the registry cannot be read
or written: a library the floor cannot find later must not be written to.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### an.library.registered_roots(, registry=None)

`(package, root)` for every root ever registered, oldest first, each once.

A line that does not parse (a torn append) is skipped; existence is the
caller’s to check. A registry that exists but cannot be read raises
[`RegistryError`](_autosummary/an.library.html.md#an.library.RegistryError).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]]

### an.library.reindex(library, , search=None)

Rebuild `library`’s floor index from its versions. Returns the number of blobs indexed.

Lineage resolves through `search` and then every library on this machine
([`an.library.floor.machine_libraries()`](_autosummary/an.library.floor.html.md#an.library.floor.machine_libraries)), so a promoted copy’s parent
in a genre’s library is read without being named (an#361).

The index is derived data: rebuilding it is always safe, and the way to
repair a library whose index was lost or written by an older `an`. It also
(re-)registers the library’s root in the machine registry
([`an.library.registry`](_autosummary/an.library.registry.html.md#module-an.library.registry)), so a library made at a custom root before the
registry existed becomes visible to every other library’s rights floor.
`reindex_changes()` says first what a rebuild would change.

The new index is computed in full first, then written over the old one
entry by entry, and only then are stale entries removed: a crash midway
leaves old and new statements side by side, never an empty floor (an#249
R4-N4).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### an.library.resolve(libraries, ref)

`(library, pinned_ref, version_doc)` for a reference, along the search path.

A namespaced reference reads only its library; a bare one reads the first
library holding the asset. `latest` resolves to that library’s head and
`sha256:<prefix>` to the one version whose manifest hash starts with it.
The returned reference is pinned (`vNNN`) and namespaced with the library
it resolved in.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library), [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.library.retire(library, asset_id, , by, reason)

Retire an asset id: recorded, hidden from `find` by default, never deleted.

Its versions stay readable — a project pinned to one still checks it out
and validates — and its rights statements still bind the floor (retiring
is curation, not a relabel). `find(status="retired")` lists it; a publish
into it is refused unless it passes `status=` to revive it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.dead", {"name": "dead"})
>>> _ = retire(lib, "prop.dead", by="me", reason="superseded")
>>> len(find(lib)), [h.asset_id for h in find(lib, status="retired")]
(0, ['prop.dead'])
```

### an.library.roll_up(sources, , inherited=())

Roll labelled sources (and parents’ rights) up to one [`Rights`](_autosummary/an.library.html.md#an.library.Rights).

A `None` source is `unknown`. The reasons name every contributor of the
winning class, so a reader sees *why* a version is restricted.

* **Return type:**
  [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights)

```pycon
>>> roll_up([("asset", None)]).to_dict()
{'license_class': 'unknown', 'publishable': False, 'reasons': ['asset: no source recorded (unknown)']}
```

### an.library.scan_index(library)

Every asset’s head version in `library`, read from the stores.

Affordances snapshotted by an older analyser are recomputed, not trusted. A
record or version that cannot be read (a damaged file) is skipped with a
`LibraryIndexWarning` naming it, so one bad entry never blinds every
search.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`IndexEntry`](_autosummary/an.library.api.html.md#an.library.api.IndexEntry)]

### an.library.search_path(package='an', , extra=(), roots=None)

The ordered libraries `package` reads: its own, then the core `an`, then `extra`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Library`](_autosummary/an.library.federation.html.md#an.library.federation.Library)]

extra: further libraries, by package name (resolved like any root) or as
: [`Library`](_autosummary/an.library.html.md#an.library.Library) objects (a team share, a seed library)

roots: an explicit root per package name (tests, a non-default layout);
: unnamed packages resolve as usual (`<PKG>_HOME`, then the data folder)

A name appears once, at its first position.

### an.library.set_status(library, asset_id, status, , by, reason)

Set an asset’s curation status, recording who and why; return the record.

The record’s `status_history` is appended to, never rewritten, and no
version is touched: a project pinned to one keeps reading it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.lamp", {"name": "lamp"})
>>> set_status(lib, "prop.lamp", "approved", by="me", reason="looks right")["status"]
'approved'
```

### an.library.sheet(refs, , libraries=None, package=None, out=None, cell=256, columns=None, parts=False, allow_private_here=False)

Write a contact sheet of `refs` (`[<library>:]<id>[@<version>]`) to one PNG.

libraries: where the references resolve (default: `package`’s search path)
out: the PNG (default `DFLT_SHEET`)
parts: tile each version’s art files instead of one specimen frame
allow_private_here: write a not-publishable sheet inside a git work tree

> that does not ignore the path
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.library.show(libraries, ref)

The record, the resolved version, its recomputed rights and the list of versions.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "prop.lamp", {"name": "lamp"}, title="A lamp")
>>> s = show(lib, "prop.lamp")
>>> s["ref"], s["record"]["title"], s["versions"]
('an:prop.lamp@v001', 'A lamp', ['v001'])
```

### an.library.verify_checkout(libraries, project_dir, , mall=None, lock=None)

`{<store>/<key>: differences}` for every pinned entry; empty lists are intact copies.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

libraries: where the pinned versions resolve (`None`: the library each
: pin names, at its default root — `pinned_libraries()`)

What makes a pin usable as more than provenance: an entry with no
differences is byte-for-byte the version its pin names. An entry whose
pinned version cannot be found, or is found with another manifest (a
same-named asset in another library), reads `["cannot verify: …"]` —
never as an edit.

### an.library.version_labels(library, version)

The labels recorded on a stored version since it was published, oldest first.

A relabel of a version’s UNCHANGED content is recorded on that version, in
the library’s append-only `labels` store, instead of minting a new
version (an#307). A label counts only for the very version it was made on:
its `manifest` must be this version’s (a same-named library’s other
`x@v001` never inherits it). An unreadable label is skipped with a
`LibraryIndexWarning`; skipping one can only leave the version
stricter.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.library.vocabulary(libraries, \*, index=<function scan_index>)

Every facet with its values and counts, and the registered capabilities.

What an agent reads to turn words into a typed query (spectrum (b)): “a
Reiniger character who can walk in profile” → `style=reiniger`,
`affords=["limbs.legs", "swap.view:side"]`. The counts are over what
`find` offers by default — the same recomputed rights, and no asset of a
hidden status (`retired`), whose numbers are under `hidden`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> v = vocabulary(lib)
>>> sorted(v)
['capabilities', 'facets', 'hidden', 'kinds', 'rights', 'statuses']
>>> isinstance(v["capabilities"], (dict, list, tuple))
True
```

### Modules

| [`affordances`](_autosummary/an.library.affordances.html.md#module-an.library.affordances)   | Affordances for the library: the capability registry, re-exported from [`an.capabilities`](_autosummary/an.capabilities.html.md#module-an.capabilities).              |
|----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`api`](_autosummary/an.library.api.html.md#module-an.library.api)                   | The library's verbs: `publish`, `find`, `vocabulary`, `show`, `promote`, `retire`.                                                                                           |
| [`cli`](_autosummary/an.library.cli.html.md#module-an.library.cli)                   | `an library …` — the asset library from the shell, over the same functions as Python.                                                                                        |
| [`federation`](_autosummary/an.library.federation.html.md#module-an.library.federation)     | Libraries federated by a search path: one read view, writes to the owner (plan §1 decision 7).                                                                               |
| [`floor`](_autosummary/an.library.floor.html.md#module-an.library.floor)               | The rights floor of a blob: the strictest statement any library on this machine makes about its bytes.                                                                       |
| [`ids`](_autosummary/an.library.ids.html.md#module-an.library.ids)                   | Asset ids, version labels and library references — the library's persisted names.                                                                                            |
| [`kinds`](_autosummary/an.library.kinds.html.md#module-an.library.kinds)               | Asset kinds: the `kind` facet's vocabulary, and where each kind lives in a project.                                                                                          |
| [`kits`](_autosummary/an.library.kits.html.md#module-an.library.kits)                 | Kits: a curated, versioned set of library assets that a project checks out in one call.                                                                                      |
| [`lock`](_autosummary/an.library.lock.html.md#module-an.library.lock)                 | The project lockfile, as the asset library sees it (re-exported from [`an.stores.library_lock`](_autosummary/an.stores.library_lock.html.md#module-an.stores.library_lock)). |
| [`registry`](_autosummary/an.library.registry.html.md#module-an.library.registry)         | The machine's memory of its libraries: every root ever written, and every statement ever made (an#249).                                                                      |
| [`rights`](_autosummary/an.library.rights.html.md#module-an.library.rights)             | Rights on every version: the most restrictive licence class wins (ADR 0005 decision 10, design §9).                                                                          |
| [`root`](_autosummary/an.library.root.html.md#module-an.library.root)                 | Where a package's library lives on disk: one root per package (ADR 0005 §2, plan §1 decisions 7–8).                                                                          |
| [`sheets`](_autosummary/an.library.sheets.html.md#module-an.library.sheets)             | `an library sheet`: a contact sheet of library versions, one specimen frame each (an#347).                                                                                   |
| [`stores`](_autosummary/an.library.stores.html.md#module-an.library.stores)             | The library mall: `records`, `versions` and `blobs`, each an injected `MutableMapping`.                                                                                      |


# _autosummary/an.library.ids.html.md

# an.library.ids

Asset ids, version labels and library references — the library’s persisted names.

Three grammars, each a persisted identifier (ADR 0005 decisions 3 and 4, plan §1
decision 7), so each is checked here once and never re-parsed by hand elsewhere:

- an **asset id** is `<kind>.<slug>` — flat, readable, filename-safe
  (`character.alice-reiniger`). The kind prefix keeps two kinds from colliding
  on one slug and makes `find(kind=…)` cheap;
- a **version label** is `v001`, `v002`, … — the studio convention, for
  people. The version’s `manifest_sha256` is its identity for machines;
- a **library reference** is `[<namespace>:]<asset_id>[@<version>]`, where
  `<version>` is a label, `latest` (floating; resolved once at check-out and
  pinned) or `sha256:<prefix>` (exact content). The namespace names the library
  an id resolves in when several are federated (`cutan:character.alice@v003`).

Because these strings become file names (`records/<asset_id>.json`,
`versions/<asset_id>/<vNNN>.json`), the grammars are also the path-traversal
guard: nothing that parses here can contain `/`, `..` or a drive letter.

```pycon
>>> ref = parse_ref("cutan:character.alice-reiniger@v003")
>>> (ref.namespace, ref.asset_id, ref.version, ref.kind)
('cutan', 'character.alice-reiniger', 'v003', 'character')
>>> str(ref)
'cutan:character.alice-reiniger@v003'
>>> version_label(12)
'v012'
```

### Module Attributes

| [`LATEST`](_autosummary/an.library.ids.html.md#an.library.ids.LATEST)        | the record's head, resolved once and then pinned.                           |
|----------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`SHA256_PREFIX`](_autosummary/an.library.ids.html.md#an.library.ids.SHA256_PREFIX) | The prefix of a content-addressed version selector (`sha256:<hex prefix>`). |

### Functions

| [`asset_kind`](_autosummary/an.library.ids.html.md#an.library.ids.asset_kind)(asset_id)                        | The kind an asset id declares by its prefix.                                                                          |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| [`check_asset_id`](_autosummary/an.library.ids.html.md#an.library.ids.check_asset_id)(asset_id)                    | Return `asset_id` if it is `<kind>.<slug>`, else raise [`AssetIdError`](_autosummary/an.library.ids.html.md#an.library.ids.AssetIdError). |
| [`check_namespace`](_autosummary/an.library.ids.html.md#an.library.ids.check_namespace)(name)                       | Return `name` if it can name a library (a package name), else raise.                                                  |
| [`check_version_label`](_autosummary/an.library.ids.html.md#an.library.ids.check_version_label)(label)                  | Return `label` if it is `vNNN`, else raise [`AssetIdError`](_autosummary/an.library.ids.html.md#an.library.ids.AssetIdError).             |
| [`parse_ref`](_autosummary/an.library.ids.html.md#an.library.ids.parse_ref)(text, \*[, require_version, ...]) | Parse a library reference.                                                                                            |
| [`version_label`](_autosummary/an.library.ids.html.md#an.library.ids.version_label)(number)                       | The label of the `number`-th version (1-based).                                                                       |
| [`version_number`](_autosummary/an.library.ids.html.md#an.library.ids.version_number)(label)                       | The number of a version label.                                                                                        |

### Classes

| [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef)(asset_id[, version, namespace])   | A parsed `[<namespace>:]<asset_id>[@<version>]`.   |
|-----------------------------------------------------------------------------------------------|----------------------------------------------------|

### Exceptions

| [`AssetIdError`](_autosummary/an.library.ids.html.md#an.library.ids.AssetIdError)   | An asset id, version label or library reference that does not parse.   |
|-----------------------------------------------------------------|------------------------------------------------------------------------|

### *exception* an.library.ids.AssetIdError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

An asset id, version label or library reference that does not parse.

### an.library.ids.LATEST *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'latest'*

the record’s head, resolved once and then pinned.

* **Type:**
  The floating version

### *class* an.library.ids.LibraryRef(asset_id, version=None, namespace=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A parsed `[<namespace>:]<asset_id>[@<version>]`.

`version` is `None` when the reference gave none (read as `latest` by
readers; refused where a pin is required, e.g. `AssetRef.library`).

#### *property* is_pinned *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether this names one immutable version (a label or a content hash).

#### *property* kind *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The asset kind, from the id’s prefix.

#### with_namespace(namespace)

The same asset and version, qualified by another library name.

* **Return type:**
  [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef)

#### with_version(version)

The same asset, another version.

* **Return type:**
  [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef)

### an.library.ids.SHA256_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'sha256:'*

The prefix of a content-addressed version selector (`sha256:<hex prefix>`).

### an.library.ids.asset_kind(asset_id)

The kind an asset id declares by its prefix.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> asset_kind("environment.palace-hall")
'environment'
```

### an.library.ids.check_asset_id(asset_id)

Return `asset_id` if it is `<kind>.<slug>`, else raise [`AssetIdError`](_autosummary/an.library.ids.html.md#an.library.ids.AssetIdError).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> check_asset_id("prop.teacup-victorian")
'prop.teacup-victorian'
>>> check_asset_id("Alice")
Traceback (most recent call last):
AssetIdError: ...
```

### an.library.ids.check_namespace(name)

Return `name` if it can name a library (a package name), else raise.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.ids.check_version_label(label)

Return `label` if it is `vNNN`, else raise [`AssetIdError`](_autosummary/an.library.ids.html.md#an.library.ids.AssetIdError).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.library.ids.parse_ref(text, , require_version=False, require_pin=False)

Parse a library reference.

`require_version=True` refuses a reference with no `@<version>`;
`require_pin=True` also refuses `@latest` — the grammar of
`AssetRef.library`, where a floating reference would make a render depend
on whatever the library’s head is that day.

* **Return type:**
  [`LibraryRef`](_autosummary/an.library.ids.html.md#an.library.ids.LibraryRef)

```pycon
>>> parse_ref("character.alice")
LibraryRef(asset_id='character.alice', version=None, namespace=None)
>>> parse_ref("character.alice@sha256:0a1b2c3d").version
'sha256:0a1b2c3d'
>>> parse_ref("character.alice", require_version=True)
Traceback (most recent call last):
AssetIdError: ...
```

### an.library.ids.version_label(number)

The label of the `number`-th version (1-based).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> version_label(1), version_label(1000)
('v001', 'v1000')
```

### an.library.ids.version_number(label)

The number of a version label.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> version_number("v012")
12
```


# _autosummary/an.library.kinds.html.md

# an.library.kinds

Asset kinds: the `kind` facet’s vocabulary, and where each kind lives in a project.

An asset id’s prefix is its kind (`character.alice`). A kind says two things the
library needs for check-out and for publishing from a folder: which project
store holds it (`characters`) and what its descriptor file is called inside a
folder (`character.json`). Kinds with no project store yet (`motion`,
`reference`, `plane`) can be published and found, not checked out. The
`kit` kind is one of them for `checkout()`: a kit is a
document naming other assets, and [`checkout_kit()`](_autosummary/an.library.kits.html.md#an.library.kits.checkout_kit) checks
out the assets it names.

Genre packages register their kinds on import (ADR 0005 decision 12); the
built-ins below are the kinds `an`’s project mall already stores, plus the ones
the design names.

```pycon
>>> asset_kind_info("character").store
'characters'
>>> asset_kind_info("motion").store is None
True
```

### Module Attributes

| [`KIT_KIND`](_autosummary/an.library.kinds.html.md#an.library.kinds.KIT_KIND)    | a versioned set of assets a production checks out together.   |
|--------------------------------------------------------------|---------------------------------------------------------------|
| [`ASSET_KINDS`](_autosummary/an.library.kinds.html.md#an.library.kinds.ASSET_KINDS) | Registered asset kinds, by name.                              |

### Functions

| [`asset_kind_info`](_autosummary/an.library.kinds.html.md#an.library.kinds.asset_kind_info)(name)                       | The registered kind `name`, or [`UnknownKindError`](_autosummary/an.library.kinds.html.md#an.library.kinds.UnknownKindError) naming the known ones.   |
|----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| [`register_asset_kind`](_autosummary/an.library.kinds.html.md#an.library.kinds.register_asset_kind)(name, \*[, store, ...]) | Register (or re-register) an asset kind.                                                                                  |

### Classes

| [`AssetKind`](_autosummary/an.library.kinds.html.md#an.library.kinds.AssetKind)(name, store, descriptor[, ...])   | One asset kind: its project store and its descriptor file name in a folder.   |
|----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|

### Exceptions

| [`UnknownKindError`](_autosummary/an.library.kinds.html.md#an.library.kinds.UnknownKindError)   | An asset id whose kind prefix nobody registered.   |
|---------------------------------------------------------------------|----------------------------------------------------|

### an.library.kinds.ASSET_KINDS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [AssetKind](_autosummary/an.library.kinds.html.md#an.library.kinds.AssetKind)]* *= {'character': AssetKind(name='character', store='characters', descriptor='character.json', credits_store='characters'), 'environment': AssetKind(name='environment', store='environments', descriptor='meta.json', credits_store='environments'), 'kit': AssetKind(name='kit', store=None, descriptor=None, credits_store=None), 'motion': AssetKind(name='motion', store=None, descriptor=None, credits_store=None), 'plane': AssetKind(name='plane', store=None, descriptor=None, credits_store=None), 'prop': AssetKind(name='prop', store='props', descriptor='prop.json', credits_store='props'), 'reference': AssetKind(name='reference', store=None, descriptor=None, credits_store=None), 'sound': AssetKind(name='sound', store='sounds', descriptor='sound.json', credits_store='sounds'), 'style': AssetKind(name='style', store='styles', descriptor=None, credits_store=None), 'voice': AssetKind(name='voice', store='voices', descriptor=None, credits_store=None)}*

Registered asset kinds, by name.

### *class* an.library.kinds.AssetKind(name, store, descriptor, credits_store=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One asset kind: its project store and its descriptor file name in a folder.

`descriptor` is `None` for kinds whose project store keeps one JSON
document per key with no folder (voices, styles): such an asset has no files.

#### credits_store *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

The `an credits` store name its sources are read under (rights roll-up);
`None` reads the top-level `source` only.

### an.library.kinds.KIT_KIND *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'kit'*

a versioned set of assets a production checks out together.

* **Type:**
  The kind of a kit

### *exception* an.library.kinds.UnknownKindError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

An asset id whose kind prefix nobody registered.

### an.library.kinds.asset_kind_info(name)

The registered kind `name`, or [`UnknownKindError`](_autosummary/an.library.kinds.html.md#an.library.kinds.UnknownKindError) naming the known ones.

* **Return type:**
  [`AssetKind`](_autosummary/an.library.kinds.html.md#an.library.kinds.AssetKind)

### an.library.kinds.register_asset_kind(name, , store=None, descriptor=None, credits_store=None)

Register (or re-register) an asset kind. Returns it.

* **Return type:**
  [`AssetKind`](_autosummary/an.library.kinds.html.md#an.library.kinds.AssetKind)


# _autosummary/an.library.kits.html.md

# an.library.kits

Kits: a curated, versioned set of library assets that a project checks out in one call.

A production in a named style needs a *set* of assets — the style’s StylePack and
voice documents, a date-card environment, recurring props, the base cast — not
one. A **kit** is the library’s answer: an asset of kind `kit` whose document
lists the members, each pinned to a concrete version, so a kit version always
means the same set of bytes. [`publish_kit()`](_autosummary/an.library.kits.html.md#an.library.kits.publish_kit) makes one; [`checkout_kit()`](_autosummary/an.library.kits.html.md#an.library.kits.checkout_kit)
checks every member out through the ordinary `checkout()`
(its pin in `assets.lock.json`, its rights, its credits) and records the kit
beside them (`ProjectLock.kits`, [`an.stores.library_lock`](_autosummary/an.stores.library_lock.html.md#module-an.stores.library_lock)).

The document (kind `Kit`, versioned like every document kind):

```json
{"kind": "Kit", "schema_version": "0.1.0", "name": "reiniger-base",
 "members": [{"ref": "style.reiniger@v003", "key": null},
             {"ref": "cutan:character.alice@v002", "key": "alice"}],
 "note": "what a Reiniger-style short starts from"}
```

Three rules, each the point rather than a detail:

- **Members are pinned.** `latest` and unversioned references are resolved when
  the kit is published, so re-checking out an old kit version never drifts. A
  member reference with no `<library>:` prefix resolves in the kit’s own
  library first, then along the search path.
- **Kits do not nest** (v1). A member of kind `kit` is refused, at publish and
  at check-out: a nested set would make the lockfile’s record of a kit’s members
  a tree, and no one has asked for one.
- **Rights stay per member.** The kit’s own document is its author’s, so its
  rights come from the `source=` the publisher gives, like any document; the
  members’ rights are NOT rolled into the kit’s record, because each member
  carries its own at check-out (and `an credits` reads them there).

A check-out resolves and checks every member before it writes any: a missing
member, a nested kit, a corrupt blob or a project entry that is a fork refuses
the whole kit and leaves the project as it was.

```pycon
>>> doc = Kit(name="base", members=[KitMember(ref="style.reiniger@v001")])
>>> [m.ref for m in doc.members], doc.schema_version
(['style.reiniger@v001'], '0.1.0')
```

### Module Attributes

| [`KIT_SCHEMA_VERSION`](_autosummary/an.library.kits.html.md#an.library.kits.KIT_SCHEMA_VERSION)   | Schema version of the kit document (its own document kind).   |
|-----------------------------------------------------------------------|---------------------------------------------------------------|

### Functions

| [`checkout_kit`](_autosummary/an.library.kits.html.md#an.library.kits.checkout_kit)(libraries, project_dir, ref, \*)      | Check the members of a kit out into a project, pin each, and record the kit.             |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [`publish_kit`](_autosummary/an.library.kits.html.md#an.library.kits.publish_kit)(library, asset_id, members, \*[, ...]) | Publish a kit — a pinned set of assets — as the next version of `asset_id` in `library`. |

### Classes

| [`Kit`](_autosummary/an.library.kits.html.md#an.library.kits.Kit)(\*\*data)       | The kit document: a name, its pinned members, and a note saying what it is for.   |
|----------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`KitMember`](_autosummary/an.library.kits.html.md#an.library.kits.KitMember)(\*\*data) | One member of a kit: a pinned reference and the project key it lands under.       |

### an.library.kits.KIT_SCHEMA_VERSION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '0.1.0'*

Schema version of the kit document (its own document kind).

### *class* an.library.kits.Kit(\*\*data)

Bases: `BaseModel`

The kit document: a name, its pinned members, and a note saying what it is for.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.library.kits.KitMember(\*\*data)

Bases: `BaseModel`

One member of a kit: a pinned reference and the project key it lands under.

`key=None` takes the check-out’s default (the asset id’s slug).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.library.kits.checkout_kit(libraries, project_dir, ref, , overwrite=False, upgrade=False, only=None, skip=(), mall=None, lock=None)

Check the members of a kit out into a project, pin each, and record the kit.

libraries: where the kit and its members resolve
project_dir: the project to check out into
ref: `[<library>:]<kit asset id>[@<version>]`; `latest` is resolved now
overwrite: replace project entries that are not exactly their member’s version
upgrade: update in place a member’s entry pinned to an earlier version of

> that member, unedited since (`checkout()`)

only: check out only these members, each named by the key it lands under
: (`narrator`) or by its asset id (`voice.narrator`); default: all

skip: leave these members out, named the same way
mall: the project mall (default: `build_project_mall(project_dir)`)
lock: the lockfile (default: the mall’s `library_lock` store); it needs a

> `kits` section, as [`ProjectLock`](_autosummary/an.stores.library_lock.html.md#an.stores.library_lock.ProjectLock) has

Returns one [`CheckoutResult`](_autosummary/an.library.html.md#an.library.CheckoutResult) per member checked
out, in the kit’s order. Each member is checked out by `checkout()`
under its `key` and pinned in `assets.lock.json` as any check-out is; the
kit itself is recorded under the lockfile’s `kits` section (its pinned
reference, manifest and the members’ lockfile keys), so `an library` readers
and a human can see which kit the project came from. Checking the same kit out
again is idempotent. A partial check-out (`only`, `skip`, an#400) records
the members the project holds: a later check-out of more members of the same
kit version adds to them.

All members are resolved and checked before the first is written: a missing
member, a member that is itself a kit, a corrupt stored file, or a project
entry that is a fork (without `overwrite`) raises `CheckoutError`
and the project is untouched.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`CheckoutResult`](_autosummary/an.library.html.md#an.library.CheckoutResult)]

### an.library.kits.publish_kit(library, asset_id, members, , search=None, name=None, note=None, \*\*curation)

Publish a kit — a pinned set of assets — as the next version of `asset_id` in `library`.

library: the owning library (writes never go to a search path)
asset_id: `kit.<slug>`
members: each a library reference (`[<library>:]<asset_id>[@<version>]`), a

> `(reference, key)` pair, or a mapping `{"ref": …, "key": …}`; `key` is
> the name the member takes in the project’s store (default: the asset’s slug)

search: further libraries where member references resolve (the owning library
: is always searched first, as in [`publish()`](_autosummary/an.library.api.html.md#an.library.api.publish))

name: the kit’s name in its document (default: the asset id’s slug)
note: what the kit is for, stored in the document and on the version
curation: everything else [`publish()`](_autosummary/an.library.api.html.md#an.library.api.publish) takes — `source=`

> (the kit document is its author’s own authoring: its rights come from
> this, never from its members), `title`, `style`, `tags`, …

Every member must resolve; `latest` and unversioned references are pinned to
the concrete version now, so the kit version is reproducible. A member that is
itself a kit, one with no project store, or two members landing in one
`(store, key)` are refused.

```pycon
>>> lib = open_library("an", records={}, versions={}, blobs={})
>>> _ = publish(lib, "style.noir", {"name": "noir"}, source={"provider": "me", "license": "cc0-1.0"})
>>> r = publish_kit(lib, "kit.noir-base", ["style.noir"], source={"provider": "me", "license": "cc0-1.0"})
>>> lib.versions["kit.noir-base@v001"]["doc"]["members"]
[{'ref': 'style.noir@v001', 'key': None}]
```

* **Return type:**
  [`PublishResult`](_autosummary/an.library.api.html.md#an.library.api.PublishResult)


# _autosummary/an.library.lock.html.md

# an.library.lock

The project lockfile, as the asset library sees it (re-exported from [`an.stores.library_lock`](_autosummary/an.stores.library_lock.html.md#module-an.stores.library_lock)).

The lockfile is a project store — `mall["library_lock"]`, registered by
[`an.stores.build_project_mall()`](_autosummary/an.stores.html.md#an.stores.build_project_mall) (an#240) — so its class lives with the other
project stores and building a project never imports the library. This module
keeps the import path the library and its callers already use.

```pycon
>>> LOCKFILE_NAME, lock_key("characters", "alice")
('assets.lock.json', 'characters/alice')
```

### Functions

| [`lock_key`](_autosummary/an.library.lock.html.md#an.library.lock.lock_key)(store, key)   | The lockfile key of one project store entry.   |
|-------------------------------------------------------------------------|------------------------------------------------|

### Classes

| [`ProjectLock`](_autosummary/an.library.lock.html.md#an.library.lock.ProjectLock)(project_dir)   | `<store>/<key> -> pin` over a project's `assets.lock.json`.   |
|-----------------------------------------------------------------------------|---------------------------------------------------------------|

### *class* an.library.lock.ProjectLock(project_dir)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

`<store>/<key> -> pin` over a project’s `assets.lock.json`.

Every write rewrites the whole (small) file, sorted, so the lockfile diffs
cleanly under version control. The mapping is the `assets` section;
[`kits`](_autosummary/an.library.lock.html.md#an.library.lock.ProjectLock.kits) is the `kits` section, and a write to either keeps the other.

#### *property* kits *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]*

`kit asset id -> record` of the kits checked out into the project.

### an.library.lock.lock_key(store, key)

The lockfile key of one project store entry.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> lock_key("characters", "alice")
'characters/alice'
```


# _autosummary/an.library.registry.html.md

# an.library.registry

The machine’s memory of its libraries: every root ever written, and every statement ever made (an#249).

The rights floor ([`an.library.floor`](_autosummary/an.library.floor.html.md#module-an.library.floor)) must read every statement any
library on this machine makes about a blob, not only those it can discover
from today’s environment. Discovery finds the package roots under the CURRENT
data folder (which follows `XDG_DATA_HOME`) and the CURRENTLY set
`<PKG>_HOME` roots. A library written at an explicit `root=` / `--root`,
or under an environment that has since changed, is invisible to discovery, and
its private bytes would then publish as `free` anywhere else.

So this module keeps two records, side by side, at a location that depends on
**nothing in the environment**: the account’s home folder as the operating
system records it (the password database on POSIX, never `$HOME`), under the
core package’s default data folder — `~/.local/share/an/registry/` on Linux
and macOS, whatever `XDG_DATA_HOME`, `AN_HOME` or `<PKG>_HOME` say. (On
Windows the account’s home is `Path.home()`, which follows `USERPROFILE`:
there the location is as stable as that variable.)

- **The registry of roots** (`library_roots.jsonl`): the first write to any
  on-disk library records its root, and the floor reads each registered root’s
  own index. JSON Lines, **append-only**: an append of one short line is
  atomic, so racing publishers cannot lose each other’s registration, and a
  line torn by a crash is skipped (the next append starts on a fresh line).
  A root that no longer exists is skipped — which is why the second record
  exists.
- **The memory of statements** (`statements/<aa>/<sha256>/…json`): every
  statement a version makes about a blob, written at publish beside the
  library’s own index. The floor reads it in addition to the libraries, so a
  library that is moved, renamed, unmounted or deleted can never relax a
  statement it once made: **a missing root relaxes nothing**. One small file
  per (blob, library root, asset), replaced only by a later version of the same
  asset at the same root — so a relicence there still speaks, as in the
  library’s own index. A file that cannot be parsed counts as `unknown`.
- **The record of generated bytes** (`generated/<aa>/<sha256>/…json`): the
  digests the character factory drew on this machine, written only at the end
  of `new_character`, for the folder it created empty in that call
  (`record_generated()`, deliberately not exported) — never by the public
  stamping functions, which stamp whatever bytes they are pointed at. It verifies a
  factory stamp: such bytes are the factory’s `cc0` work whatever an older,
  UNLABELLED asset holding the same bytes fails to say (an#269). It relaxes
  only `unknown`, never a statement anyone made.

Deleting the registry folder wholesale deletes that memory with it — nothing
left on disk tells a deleted registry from a first use — so whenever the
registry is created from nothing, the first write warns
(`RegistryWarning`). Prune it; never delete it.

Both are read **fail-closed**: a registry or memory that exists but cannot be
read raises [`RegistryError`](_autosummary/an.library.registry.html.md#an.library.registry.RegistryError) — a floor that silently read less would let
private bytes out. Libraries with injected stores (no root on disk: an S3 or
in-memory mall) are in neither record; their statements reach the floor only
while they are on the search path.

Neither record is a search path: a registered library’s assets are never
found, resolved or checked out through them — reads still go through the
explicit search path ([`an.library.federation.search_path()`](_autosummary/an.library.federation.html.md#an.library.federation.search_path)).

Tests and doctests are redirected by the repository’s root `conftest.py`,
which points `_account_home()` into a temporary folder. A test that runs
`an` in a SUBPROCESS cannot be redirected that way and must not publish.

```pycon
>>> import tempfile, pathlib
>>> with tempfile.TemporaryDirectory() as d:
...     reg = pathlib.Path(d) / "roots.jsonl"
...     first = register_root("cutan", pathlib.Path(d) / "study", registry=reg)
...     again = register_root("cutan", pathlib.Path(d) / "study", registry=reg)
...     (first, again), [(p, r.name) for p, r in registered_roots(registry=reg)]
((True, False), [('cutan', 'study')])
```

### Module Attributes

| [`REGISTRY_DIRNAME`](_autosummary/an.library.registry.html.md#an.library.registry.REGISTRY_DIRNAME)   | The sub-folder of the core package's default data root holding the registry (a root holds one sub-folder per kind of data, never files at the top).   |
|---------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`REGISTRY_FILENAME`](_autosummary/an.library.registry.html.md#an.library.registry.REGISTRY_FILENAME)  | JSON Lines, one `{"package", "root", "registered"}` per line.                                                                                         |
| [`STATEMENTS_DIRNAME`](_autosummary/an.library.registry.html.md#an.library.registry.STATEMENTS_DIRNAME) | The folder of remembered statements, beside the registry file.                                                                                        |
| [`GENERATED_DIRNAME`](_autosummary/an.library.registry.html.md#an.library.registry.GENERATED_DIRNAME)  | The folder of the machine's record of bytes a GENERATOR drew here (an#269).                                                                           |

### Functions

| [`machine_registry_dir`](_autosummary/an.library.registry.html.md#an.library.registry.machine_registry_dir)()                       | The folder holding the registry and the statement memory (independent of the environment).                                                                                              |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`machine_registry_path`](_autosummary/an.library.registry.html.md#an.library.registry.machine_registry_path)()                      | Where this machine's registry of library roots lives.                                                                                                                                   |
| [`register_root`](_autosummary/an.library.registry.html.md#an.library.registry.register_root)(package, root, \*[, registry]) | Record `root` (`package`'s library root) in the registry; `True` if it was new.                                                                                                         |
| [`registered_roots`](_autosummary/an.library.registry.html.md#an.library.registry.registered_roots)(\*[, registry])             | `(package, root)` for every root ever registered, oldest first, each once.                                                                                                              |
| [`remember_statements`](_autosummary/an.library.registry.html.md#an.library.registry.remember_statements)(root, statements)        | Remember `(digest, asset_key, statement)` made by the library at `root`.                                                                                                                |
| [`remembered_statements`](_autosummary/an.library.registry.html.md#an.library.registry.remembered_statements)(digest)                | `(asset_key, statement)` for every statement this machine ever recorded about `digest`.                                                                                                 |
| [`remembered_statements_by_root`](_autosummary/an.library.registry.html.md#an.library.registry.remembered_statements_by_root)(digest)        | `(root, asset_key, statement)`: [`remembered_statements()`](_autosummary/an.library.registry.html.md#an.library.registry.remembered_statements) with the resolved root of the library that made each (`None` when unreadable). |
| [`generated_by`](_autosummary/an.library.registry.html.md#an.library.registry.generated_by)(digest)                         | The generators this machine recorded as having drawn the bytes `digest`.                                                                                                                |

### Exceptions

| [`RegistryError`](_autosummary/an.library.registry.html.md#an.library.registry.RegistryError)   | The machine's registry or statement memory cannot be read or written; nothing proceeds.   |
|------------------------------------------------------------------|-------------------------------------------------------------------------------------------|

### an.library.registry.GENERATED_DIRNAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'generated'*

The folder of the machine’s record of bytes a GENERATOR drew here (an#269).

### an.library.registry.REGISTRY_DIRNAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'registry'*

The sub-folder of the core package’s default data root holding the registry
(a root holds one sub-folder per kind of data, never files at the top).

### an.library.registry.REGISTRY_FILENAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'library_roots.jsonl'*

JSON Lines, one `{"package", "root", "registered"}` per line.

* **Type:**
  The registry file

### *exception* an.library.registry.RegistryError

Bases: [`OSError`](https://docs.python.org/3/builtins/exceptions.html#OSError)

The machine’s registry or statement memory cannot be read or written; nothing proceeds.

### an.library.registry.STATEMENTS_DIRNAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'statements'*

The folder of remembered statements, beside the registry file.

### an.library.registry.generated_by(digest)

The generators this machine recorded as having drawn the bytes `digest`.

Read fail-safe for rights: anything unreadable counts as not recorded, so
the stamp it would verify is not verified (the stricter reading).

* **Return type:**
  [`frozenset`](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.library.registry.machine_registry_dir()

The folder holding the registry and the statement memory (independent of the environment).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> machine_registry_dir().name
'registry'
```

### an.library.registry.machine_registry_path()

Where this machine’s registry of library roots lives.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> machine_registry_path().name
'library_roots.jsonl'
```

### an.library.registry.register_root(package, root, , registry=None)

Record `root` (`package`’s library root) in the registry; `True` if it was new.

Idempotent. Raises [`RegistryError`](_autosummary/an.library.registry.html.md#an.library.registry.RegistryError) when the registry cannot be read
or written: a library the floor cannot find later must not be written to.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### an.library.registry.registered_roots(, registry=None)

`(package, root)` for every root ever registered, oldest first, each once.

A line that does not parse (a torn append) is skipped; existence is the
caller’s to check. A registry that exists but cannot be read raises
[`RegistryError`](_autosummary/an.library.registry.html.md#an.library.registry.RegistryError).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]]

### an.library.registry.remember_statements(root, statements)

Remember `(digest, asset_key, statement)` made by the library at `root`.

One file per (digest, root, asset), replaced only by a statement of a later
(or the same) version of that asset — the same rule as a library’s own
index. Raises [`RegistryError`](_autosummary/an.library.registry.html.md#an.library.registry.RegistryError) when it cannot be written.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.library.registry.remembered_statements(digest)

`(asset_key, statement)` for every statement this machine ever recorded about `digest`.

A remembered file that cannot be parsed counts as an `unknown`
statement (it said something; what is no longer known). A memory that
exists but cannot be listed raises [`RegistryError`](_autosummary/an.library.registry.html.md#an.library.registry.RegistryError).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]]

### an.library.registry.remembered_statements_by_root(digest)

`(root, asset_key, statement)`: [`remembered_statements()`](_autosummary/an.library.registry.html.md#an.library.registry.remembered_statements) with the
resolved root of the library that made each (`None` when unreadable).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]]


# _autosummary/an.library.rights.html.md

# an.library.rights

Rights on every version: the most restrictive licence class wins (ADR 0005 decision 10, design §9).

The library adds no licence vocabulary. A version’s rights are rolled up from
`an`’s existing model (an#211): [`AssetSource`](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) records and
[`license_class()`](_autosummary/an.ir.assets.html.md#an.ir.assets.license_class)’s four classes. The roll-up reads them from

- the source declared for the asset as a whole at publish, AND the descriptor’s
  own `source` — both, never one instead of the other (an asset with neither
  is `unknown`: recorded and visible, never rejected);
- every part and plane that declares its own `source` — gathered by
  [`an.credits.collect_credits()`](_autosummary/an.credits.html.md#an.credits.collect_credits), the same walk `an credits` runs, so the
  > library and the credits report can never disagree about what an asset holds;
- every version it derives from (`derived_from`) and the version it follows
  (`previous`), recursively, by the same rules: a derivative inherits the
  obligations of its sources, and a new version cannot relabel the old one;
- the source any other version records for the same file bytes (by blob
  digest): a file’s own recorded provenance travels with its bytes, so carved
  parts published again under a new id keep their obligations
  ([`an.library.api.version_sources()`](_autosummary/an.library.api.html.md#an.library.api.version_sources) walks all of it).

A version may only ever be MORE restrictive than what it inherits. Relaxing
takes an explicit, recorded relicence — who and why — on the version. Where
nobody ever said anything (a file an earlier version recorded `unlabelled`,
a version with no source), an explicit source with a recorded `relabel` —
who and why — is the first statement (an#263); it relaxes nothing anyone said.

Order, most restrictive first: `private` > `unknown` > `attribution` >
`free`. `private` and `unknown` are not publishable.

```pycon
>>> from an.ir.assets import AssetSource
>>> r = roll_up([("asset", AssetSource(provider="p", license="cc0-1.0")),
...              ("parts/head.png", AssetSource(provider="film", license="all-rights-reserved"))])
>>> (r.license_class, r.publishable)
('private', False)
>>> r.reasons
['parts/head.png: all-rights-reserved (private)']
```

### Module Attributes

| [`LICENSE_CLASS_ORDER`](_autosummary/an.library.rights.html.md#an.library.rights.LICENSE_CLASS_ORDER)   | Licence classes, most restrictive first.                                                                                                                                                                  |
|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`PUBLISHABLE_CLASSES`](_autosummary/an.library.rights.html.md#an.library.rights.PUBLISHABLE_CLASSES)   | The classes a video may ship with (`attribution` and `noncommercial` with their credit displayed; `noncommercial` only in a video nobody earns from — an#373: flagged for commercial use, never blocked). |
| [`COMMERCIAL_CLASSES`](_autosummary/an.library.rights.html.md#an.library.rights.COMMERCIAL_CLASSES)    | The classes a COMMERCIAL video may ship with (monetised, sponsored, client work).                                                                                                                         |

### Functions

| [`most_restrictive`](_autosummary/an.library.rights.html.md#an.library.rights.most_restrictive)(classes)                   | The most restrictive of `classes`; `free` for none.                                                            |
|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| [`descriptor_source`](_autosummary/an.library.rights.html.md#an.library.rights.descriptor_source)(doc, \*, store)           | The source the descriptor itself declares (or `an credits` reconstructs), if any.                              |
| [`roll_up`](_autosummary/an.library.rights.html.md#an.library.rights.roll_up)(sources, \*[, inherited])           | Roll labelled sources (and parents' rights) up to one [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights). |
| [`sources_in`](_autosummary/an.library.rights.html.md#an.library.rights.sources_in)(doc, \*, store[, source, files]) | Every labelled source one version holds: the asset's, the descriptor's, each part's.                           |

### Classes

| [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights)(license_class[, reasons])   | The rolled-up rights of one version, as stored on it.   |
|-------------------------------------------------------------------------------------|---------------------------------------------------------|

### Exceptions

| [`RightsRefusal`](_autosummary/an.library.rights.html.md#an.library.rights.RightsRefusal)   | A private or unknown version would leave the user's library without an override.   |
|------------------------------------------------------------------|------------------------------------------------------------------------------------|

### an.library.rights.COMMERCIAL_CLASSES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'attribution', 'free'})*

The classes a COMMERCIAL video may ship with (monetised, sponsored, client work).

### an.library.rights.LICENSE_CLASS_ORDER *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['attribution', 'free', 'noncommercial', 'private', 'unknown'], ...]* *= ('private', 'unknown', 'noncommercial', 'attribution', 'free')*

Licence classes, most restrictive first. The roll-up keeps the first that occurs.

### an.library.rights.PUBLISHABLE_CLASSES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'attribution', 'free', 'noncommercial'})*

The classes a video may ship with (`attribution` and `noncommercial`
with their credit displayed; `noncommercial` only in a video nobody earns
from — an#373: flagged for commercial use, never blocked).

### *class* an.library.rights.Rights(license_class, reasons=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The rolled-up rights of one version, as stored on it.

#### *property* commercial *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether a COMMERCIAL video containing this asset may ship (an#373).

#### *classmethod* from_dict(d)

Read a version’s `rights` block back.

* **Return type:**
  [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights)

#### *property* publishable *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether a video containing this asset may ship (commercially or not).

#### to_dict()

The `rights` block of a version document.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *exception* an.library.rights.RightsRefusal

Bases: [`PermissionError`](https://docs.python.org/3/builtins/exceptions.html#PermissionError)

A private or unknown version would leave the user’s library without an override.

### an.library.rights.descriptor_source(doc, , store)

The source the descriptor itself declares (or `an credits` reconstructs), if any.

* **Return type:**
  [`AssetSource`](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.library.rights.most_restrictive(classes)

The most restrictive of `classes`; `free` for none.

* **Return type:**
  [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'attribution'`, `'free'`, `'noncommercial'`, `'private'`, `'unknown'`]

```pycon
>>> most_restrictive(["free", "attribution"]), most_restrictive([])
('attribution', 'free')
```

### an.library.rights.roll_up(sources, , inherited=())

Roll labelled sources (and parents’ rights) up to one [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights).

A `None` source is `unknown`. The reasons name every contributor of the
winning class, so a reader sees *why* a version is restricted.

* **Return type:**
  [`Rights`](_autosummary/an.library.rights.html.md#an.library.rights.Rights)

```pycon
>>> roll_up([("asset", None)]).to_dict()
{'license_class': 'unknown', 'publishable': False, 'reasons': ['asset: no source recorded (unknown)']}
```

### an.library.rights.sources_in(doc, , store, source=None, files=None)

Every labelled source one version holds: the asset’s, the descriptor’s, each part’s.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`AssetSource`](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]]

store: the project store this kind of document lives in (`characters`,
: `props`, `environments`, `sounds`) — its sources are read by
  `an credits`’ own walk; `None` reads the top-level `source` alone

source: a source declared for the asset as a whole (at publish, or carried
: from an earlier version). It is a contributor BESIDE the descriptor’s
  own source, never instead of it: the roll-up takes the most
  restrictive, so a declared `cc0` cannot mask a descriptor that says
  the art came from a film. The only way to relax a stricter source is
  an explicit, recorded relicence ([`an.library.api.publish()`](_autosummary/an.library.api.html.md#an.library.api.publish)’s
  `relicense`), which bypasses this function altogether

files: `{path: sha256}` of the asset’s files. With them, a factory stamp
: is checked against the bytes it pins (`an.credits._part_credits()`):
  a re-carved part under a stale stamp, or a file the factory’s
  descriptor stamp does not pin, is `unknown` unless a source covers it

With no source anywhere the asset contributes `None`: `unknown`.

```pycon
>>> private = {"provider": "film", "license": "all-rights-reserved"}
>>> [label for label, _ in sources_in({"source": private}, store=None,
...                                   source=AssetSource(provider="me", license="cc0-1.0"))]
['asset', 'descriptor']
```


# _autosummary/an.library.root.html.md

# an.library.root

Where a package’s library lives on disk: one root per package (ADR 0005 §2, plan §1 decisions 7–8).

The root is resolved in this order, first match wins:

1. an explicit `root=` (tests, power users, a team share);
2. the environment variable `<PKG>_HOME` (`AN_HOME`, `CUTAN_HOME`) — one
   switch for a whole shell or CI job;
3. the platform data folder: `$XDG_DATA_HOME/<pkg>` when `XDG_DATA_HOME` is
   set to an absolute path, else `~/.local/share/<pkg>` on Linux and macOS;
   `%LOCALAPPDATA%\<pkg>` on Windows. On macOS this is deliberately
   `~/.local/share`, not `~/Library/Application Support` — the maintainer’s
   choice, and the one the fleet’s storage rules use.

This is the data-folder half of `config2py.get_app_folder`, **vendored** (plan
§1 decision 8): twenty lines are cheaper than a dependency, and the dependency is
taken only if more than this is ever needed.

Under the root, one sub-folder per kind of data, never files at the top:
`library/` (records, versions, blobs) and `projects/` (agent-made videos).
Resolving a root never creates it and never raises at import; the stores create
their folders on first use.

```pycon
>>> env = {"AN_HOME": "/srv/an-lib"}
>>> library_root(package="an", environ=env).as_posix()
'/srv/an-lib'
>>> library_root(package="cutan", environ={"XDG_DATA_HOME": "/data"}, platform="linux").as_posix()
'/data/cutan'
```

### Module Attributes

| [`CORE_PACKAGE`](_autosummary/an.library.root.html.md#an.library.root.CORE_PACKAGE)     | The core package, whose library every genre's search path reads after its own.   |
|-------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`LIBRARY_DIRNAME`](_autosummary/an.library.root.html.md#an.library.root.LIBRARY_DIRNAME)  | The sub-folder of a root that holds the library stores.                          |
| [`PROJECTS_DIRNAME`](_autosummary/an.library.root.html.md#an.library.root.PROJECTS_DIRNAME) | The sub-folder of a root that holds agent-made projects (design §7.5).           |

### Functions

| [`git_worktree_of`](_autosummary/an.library.root.html.md#an.library.root.git_worktree_of)(path)                             | The git work tree containing `path` (or its nearest existing ancestor), if any.   |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`home_env_var`](_autosummary/an.library.root.html.md#an.library.root.home_env_var)(package)                             | The override variable for `package`'s root.                                       |
| [`library_root`](_autosummary/an.library.root.html.md#an.library.root.library_root)([root, package, environ, platform])  | The data root of `package` — its library and its projects live under it.          |
| [`project_dir`](_autosummary/an.library.root.html.md#an.library.root.project_dir)(project_id[, root, package, environ]) | The default directory of the agent-made project `project_id` (design §7.5).       |
| [`projects_root`](_autosummary/an.library.root.html.md#an.library.root.projects_root)([root, package, environ])           | Where agent-made projects go by default: `<root>/projects/`.                      |

### Exceptions

| [`LibraryLocationWarning`](_autosummary/an.library.root.html.md#an.library.root.LibraryLocationWarning)   | Library data, or private material checked out of it, sits inside a git work tree.   |
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------|

### an.library.root.CORE_PACKAGE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an'*

The core package, whose library every genre’s search path reads after its own.

### an.library.root.LIBRARY_DIRNAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'library'*

The sub-folder of a root that holds the library stores.

### *exception* an.library.root.LibraryLocationWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

Library data, or private material checked out of it, sits inside a git work tree.

ADR 0005 decision 2: a library root is never inside a repository. The default
guarantees it; an explicit `root`, `<PKG>_HOME` or a project in a repo
can break it, and the library may hold private-study material.

### an.library.root.PROJECTS_DIRNAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'projects'*

The sub-folder of a root that holds agent-made projects (design §7.5).

### an.library.root.git_worktree_of(path)

The git work tree containing `path` (or its nearest existing ancestor), if any.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     git_worktree_of(d) is None
True
```

### an.library.root.home_env_var(package)

The override variable for `package`’s root.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> home_env_var("an"), home_env_var("my-genre")
('AN_HOME', 'MY_GENRE_HOME')
```

### an.library.root.library_root(root=None, , package='an', environ=None, platform=None)

The data root of `package` — its library and its projects live under it.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

root: an explicit root; wins over everything
package: the package whose root this is (`an` for the core library, a

> genre’s own name for its library)

environ: the environment to read (default `os.environ`; injectable for tests)
platform: `sys.platform` value to resolve for (default: this one)

```pycon
>>> library_root("~/x", package="an").name
'x'
```

### an.library.root.project_dir(project_id, root=None, , package='an', environ=None)

The default directory of the agent-made project `project_id` (design §7.5).

`an init <dir>` with an explicit directory keeps working anywhere; this is
the default for projects an agent makes (`an init --id <id>`), so they
never land in a session’s working folder or a repository. `package` is
the library package of the genre the video is made in
([`an.genres.genre_library()`](_autosummary/an.genres.html.md#an.genres.genre_library); the core’s, `an`, by default — the core
names no genre).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> project_dir("alice-and-bob", "/lib").as_posix()
'/lib/projects/alice-and-bob'
>>> project_dir("../escape", "/lib")
Traceback (most recent call last):
ValueError: ...
```

### an.library.root.projects_root(root=None, , package='an', environ=None)

Where agent-made projects go by default: `<root>/projects/`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> projects_root("/lib", package="cutan").as_posix()
'/lib/projects'
```


# _autosummary/an.library.sheets.html.md

# an.library.sheets

`an library sheet`: a contact sheet of library versions, one specimen frame each (an#347).

For each reference the version is checked out into a temporary project and
its kind is asked for a specimen — a short shot showing the entity on its own
([`an.genres.EntityKind.specimen`](_autosummary/an.genres.html.md#an.genres.EntityKind.specimen); the core gives props, text blocks and
environments one, a genre its own kinds) — whose first frame is drawn by
[`an.probe.frame()`](_autosummary/an.probe.html.md#an.probe.frame), the path `render` draws with. A kind without a
specimen (a voice, a style, a sound, a kind whose genre gives none) shows a
labelled placeholder. `parts=True` tiles the version’s art files instead.

Every cell is captioned with its reference and licence class, the stricter of
the version’s stored and recomputed rights, marked when the version records
a rights conflict about it (an#357). A sheet showing anything not
publishable (private or unknown) is refused at a path inside a git work tree
that does not ignore it (`an.library.root.check_private_output()`).

### Module Attributes

| [`DFLT_SHEET`](_autosummary/an.library.sheets.html.md#an.library.sheets.DFLT_SHEET)   | Where a sheet is written by default (relative to the current folder).   |
|---------------------------------------------------------------|-------------------------------------------------------------------------|

### Functions

| [`sheet`](_autosummary/an.library.sheets.html.md#an.library.sheets.sheet)(refs, \*[, libraries, package, out, ...])   | Write a contact sheet of `refs` (`[<library>:]<id>[@<version>]`) to one PNG.   |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|

### an.library.sheets.DFLT_SHEET *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'artifacts/probes/sheet.png'*

Where a sheet is written by default (relative to the current folder).

### an.library.sheets.sheet(refs, , libraries=None, package=None, out=None, cell=256, columns=None, parts=False, allow_private_here=False)

Write a contact sheet of `refs` (`[<library>:]<id>[@<version>]`) to one PNG.

libraries: where the references resolve (default: `package`’s search path)
out: the PNG (default [`DFLT_SHEET`](_autosummary/an.library.sheets.html.md#an.library.sheets.DFLT_SHEET))
parts: tile each version’s art files instead of one specimen frame
allow_private_here: write a not-publishable sheet inside a git work tree

> that does not ignore the path
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.library.stores.html.md

# an.library.stores

The library mall: `records`, `versions` and `blobs`, each an injected `MutableMapping`.

ADR 0005 decision 11 and design §10. Three entities, three stores:

| store    | key                         | on disk (default backend)                       |
|----------|-----------------------------|-------------------------------------------------|
| records  | `<asset_id>`                | `library/records/<asset_id>.json`               |
| versions | `<asset_id>@<vNNN>`         | `library/versions/<asset_id>/<vNNN>.json`       |
| blobs    | `<sha256>`                  | `library/blobs/<aa>/<sha256>`                   |
| labels   | `<asset_id>@<vNNN>/<hex16>` | `library/labels/<asset_id>/<vNNN>/<hex16>.json` |

`labels` is write-once too: the append-only statements made about an existing
version after it was published (a relabel of its unchanged content, an#307),
each its own document, never rewritten or deleted.

- **\`\`dol\`\` stores**, unlike the project mall’s hand-written folder classes: a
  byte store ([`LocalFiles`](_autosummary/an.library.stores.html.md#an.library.stores.LocalFiles)) seen through `dol.wrap_kvs()` with a JSON
  codec and a key transform. Each store is replaced by injection
  (`build_library_mall(blobs=my_s3_store)`), and business logic sees only the
  mapping interface — the move to S3 is an injection, not a rewrite.
  [`LocalFiles`](_autosummary/an.library.stores.html.md#an.library.stores.LocalFiles) rather than `dol.Files` because the library needs three
  > things `dol.Files` does not give: writes that cannot tear (temp file +
  > `os.replace`), an exclusive create for write-once versions, and containment
  > (`dol.Files` writes a `../x` key outside its root).
- **\`\`versions\`\` is write-once** ([`WriteOnce`](_autosummary/an.library.stores.html.md#an.library.stores.WriteOnce)): a version is immutable, so
  overwriting or deleting one raises [`VersionExistsError`](_autosummary/an.library.stores.html.md#an.library.stores.VersionExistsError) — projects pin
  versions, and a pin that could change under them is no pin. On the folder
  backend the create is exclusive, so racing publishers cannot both win.
- **\`\`blobs\`\` is content-addressed and undeletable** ([`ImmutableBlobs`](_autosummary/an.library.stores.html.md#an.library.stores.ImmutableBlobs),
  a `dol.content.ContentAddressedStore`): every file of every version once,
  keyed by its SHA-256; a key can never point at changed bytes, and a blob a
  version pins cannot be deleted from under it.
- **Keys are validated twice** before they become paths: by the id grammars
  ([`an.library.ids`](_autosummary/an.library.ids.html.md#module-an.library.ids)), and by [`LocalFiles`](_autosummary/an.library.stores.html.md#an.library.stores.LocalFiles)’ containment check.
- **Nothing is created until the first write.**

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     lib = build_library_mall(d)
...     sorted(lib)
['blob_rights', 'blobs', 'labels', 'records', 'versions']
>>> mem = build_library_mall(records={}, versions={}, blobs={})  # all in memory
>>> ref = mem["blobs"].add(b"<svg/>")
>>> mem["blobs"][ref.item_id]
b'<svg/>'
```

### Module Attributes

| [`LIBRARY_STORES`](_autosummary/an.library.stores.html.md#an.library.stores.LIBRARY_STORES)   | The three stores every library mall holds.   |
|-------------------------------------------------------------------|----------------------------------------------|

### Functions

| [`build_library_mall`](_autosummary/an.library.stores.html.md#an.library.stores.build_library_mall)([root, package])    | The library mall of `package`: `records`, `versions` (write-once), `blobs` (CAS).   |
|-----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| [`canonical_json`](_autosummary/an.library.stores.html.md#an.library.stores.canonical_json)(obj, \*[, indent])      | JSON with sorted keys and no locale or platform dependence.                         |
| [`label_key`](_autosummary/an.library.stores.html.md#an.library.stores.label_key)(asset_id, version, label_id) | The `labels` key of one statement about a version.                                  |
| [`split_label_key`](_autosummary/an.library.stores.html.md#an.library.stores.split_label_key)(key)                   | `(asset_id, version, label_id)` of a `labels` key, validated.                       |
| [`version_key`](_autosummary/an.library.stores.html.md#an.library.stores.version_key)(asset_id, version)         | The `versions` key of one version.                                                  |
| [`split_version_key`](_autosummary/an.library.stores.html.md#an.library.stores.split_version_key)(key)                 | `(asset_id, version)` of a `versions` key, validated.                               |

### Classes

| [`ImmutableBlobs`](_autosummary/an.library.stores.html.md#an.library.stores.ImmutableBlobs)([store, hasher, length, field])   | The content-addressed blob store, with deletion refused.                        |
|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`LocalFiles`](_autosummary/an.library.stores.html.md#an.library.stores.LocalFiles)(root)                                 | `relative/posix/path -> bytes` under one folder, with atomic, contained writes. |
| [`WriteOnce`](_autosummary/an.library.stores.html.md#an.library.stores.WriteOnce)(store)                                 | A mapping whose keys, once written, can be neither overwritten nor deleted.     |

### Exceptions

| [`VersionExistsError`](_autosummary/an.library.stores.html.md#an.library.stores.VersionExistsError)   | A write-once key was written twice, or deleted.   |
|-----------------------------------------------------------------------|---------------------------------------------------|

### *class* an.library.stores.ImmutableBlobs(store=None, \*, hasher=<built-in function openssl_sha256>, length=None, field='content')

Bases: `ContentAddressedStore`

The content-addressed blob store, with deletion refused.

Versions are write-once and pin their files by hash, so deleting a blob
would leave a published version pointing at nothing. Reclaiming unreferenced
blobs is a maintenance job (a garbage collector that counts references), not
a mapping operation.

```pycon
>>> blobs = ImmutableBlobs({})
>>> ref = blobs.add(b"x")
>>> del blobs[ref.item_id]
Traceback (most recent call last):
VersionExistsError: ...
```

### an.library.stores.LIBRARY_STORES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('records', 'versions', 'blobs')*

The three stores every library mall holds.

### *class* an.library.stores.LocalFiles(root)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

`relative/posix/path -> bytes` under one folder, with atomic, contained writes.

The default backend of every library store:

- **contained**: a key that would resolve outside the folder (`..`, an
  absolute path, a drive) raises `KeyError` and never touches the disk;
- **atomic**: a write goes to a temp file in the target folder and is moved
  into place with `os.replace`, so a crash never leaves a torn document;
- **create-only** ([`create_only()`](_autosummary/an.library.stores.html.md#an.library.stores.LocalFiles.create_only)): a hard link of the fully written temp
  file, which fails if the target exists — the primitive [`WriteOnce`](_autosummary/an.library.stores.html.md#an.library.stores.WriteOnce)
  uses so two publishers cannot both create one version;
- **lazy**: nothing is created until the first write, so opening (or
  mistyping) a library never creates folders.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     f = LocalFiles(d + "/lib")
...     f["a/b.json"] = b"{}"
...     sorted(f), f["a/b.json"]
(['a/b.json'], b'{}')
```

#### create_only(key, data)

Write `key` only if it does not exist — atomically, in one step.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *exception* an.library.stores.VersionExistsError

Bases: [`KeyError`](https://docs.python.org/3/builtins/exceptions.html#KeyError)

A write-once key was written twice, or deleted. Versions are immutable.

### *class* an.library.stores.WriteOnce(store)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

A mapping whose keys, once written, can be neither overwritten nor deleted.

Wraps any injected `MutableMapping`. Rewriting a key with the *same* value
is refused too: immutability is about the key, and a silent no-op would hide
a caller that believes it is replacing something.

**Atomicity.** When the wrapped store offers `create_only(key, value)` — an
exclusive create that raises [`VersionExistsError`](_autosummary/an.library.stores.html.md#an.library.stores.VersionExistsError) if the key exists —
every write goes through it, so two publishers racing for one key cannot both
win. The default folder backend has one (a hard link of a fully written temp
file); an S3 backend would map it to a conditional put (`If-None-Match: *`).
A store without it gets a check-then-set, which is correct for one writer.

```pycon
>>> versions = WriteOnce({})
>>> versions["character.a@v001"] = {"x": 1}
>>> versions["character.a@v001"] = {"x": 2}
Traceback (most recent call last):
VersionExistsError: ...
```

### an.library.stores.build_library_mall(root=None, , package='an', \*\*overrides)

The library mall of `package`: `records`, `versions` (write-once), `blobs` (CAS).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)]

root: the package’s data root (default: resolved by
: [`an.library.root.library_root()`](_autosummary/an.library.root.html.md#an.library.root.library_root) — `root` → `<PKG>_HOME` → the
  platform data folder); the stores live under `<root>/library/`

package: whose library this is (`an`, or a genre such as `cutan`)
overrides: a store per name to inject instead of the folder default — a

> `dict` for tests, an S3 or database mapping later. An injected
> `versions` is still made write-once and an injected `blobs` still
> content-addressed and undeletable, so injection cannot drop an invariant.

Nothing is created until the first write: opening a library (or mistyping
one on a search path) leaves the disk as it was.

### an.library.stores.canonical_json(obj, , indent=None)

JSON with sorted keys and no locale or platform dependence.

With `indent=None` it is the compact form hashed for a manifest; with an
indent it is the on-disk form (same content, readable).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> canonical_json({"b": 1, "a": [1, 2]})
'{"a":[1,2],"b":1}'
```

### an.library.stores.label_key(asset_id, version, label_id)

The `labels` key of one statement about a version.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> label_key("character.alice", "v002", "0123456789abcdef")
'character.alice@v002/0123456789abcdef'
```

### an.library.stores.split_label_key(key)

`(asset_id, version, label_id)` of a `labels` key, validated.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> split_label_key("character.alice@v002/0123456789abcdef")
('character.alice', 'v002', '0123456789abcdef')
```

### an.library.stores.split_version_key(key)

`(asset_id, version)` of a `versions` key, validated.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> split_version_key("character.alice@v002")
('character.alice', 'v002')
```

### an.library.stores.version_key(asset_id, version)

The `versions` key of one version.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> version_key("character.alice", "v002")
'character.alice@v002'
```


# _autosummary/an.live_api.html.md

# an.live_api

The one switch that says “yes, this run may spend money”.

A key being present is not consent to spend. `ELEVEN_API_KEY` (or
`ANTHROPIC_API_KEY`) is exported by every developer shell that has ever
sourced a profile, and by every unattended agent session started from one — so
“has a key” describes the exact population that must *not* be billed silently.
The federation adopted a rule about this after a real incident (video_gen
decision D-vg-audio-02): a code path that can reach a paid API needs an
**explicit positive opt-in env var in addition to the key**.

[`LIVE_API_ENV_VAR`](_autosummary/an.live_api.html.md#an.live_api.LIVE_API_ENV_VAR) is that signal, and this module is its single
definition. `tests/conftest.py` reads it for the `live_api` marker, and
`examples/character_gallery/build.py` reads it before choosing a paid TTS
provider.

**The perimeter this actually draws.** The rule is about provider
*auto-detection* — code that picks a paid provider because a key happened to be
in the environment. It is not a ban on paid work, and two shipped paths reach
paid APIs without consulting this predicate because the caller named them:
`an iterate` (Anthropic) and the vision verifier’s default judge — and a third
whose project named it: `an render` speaks a voice whose document declares
`provider: elevenlabs` with ElevenLabs (an#305), printing the requests and
characters it will bill before the first one, and never billing a cached line. If you add a
path that CHOOSES a provider rather than being told one, it belongs here — an example is not a test, but it is the file with the most footfall,
and a clean checkout has a cold audio cache, so every line it speaks is a new
charge.

There is deliberately **one** switch, not one per caller: a second variable
would be a second answer to “may this run spend?”, and the two would drift.

```pycon
>>> LIVE_API_ENV_VAR
'AN_LIVE_API_TESTS'
```

### Module Attributes

| [`LIVE_API_ENV_VAR`](_autosummary/an.live_api.html.md#an.live_api.LIVE_API_ENV_VAR)   | Set this truthy to opt a run in to real, billed API calls.   |
|---------------------------------------------------------------------|--------------------------------------------------------------|
| [`TRUTHY_VALUES`](_autosummary/an.live_api.html.md#an.live_api.TRUTHY_VALUES)      | Accepted spellings of "yes".                                 |
| [`CI_ENV_VAR`](_autosummary/an.live_api.html.md#an.live_api.CI_ENV_VAR)         | Set by every CI provider we care about.                      |

### Functions

| [`live_api_enabled`](_autosummary/an.live_api.html.md#an.live_api.live_api_enabled)([env])   | Whether this run has explicitly opted in to paid API calls.                                                                                                                                                                                                      |
|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`paid_provider_key_vars`](_autosummary/an.live_api.html.md#an.live_api.paid_provider_key_vars)()  | Every environment variable a paid provider reads its API key from — the `env.key.*` capabilities' ([`an.capabilities.subjects.ENV_KEYS`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENV_KEYS)), so a new paid provider is listed once. |

### an.live_api.CI_ENV_VAR *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'CI'*

Set by every CI provider we care about. CI must never spend, whatever else
is configured, because nobody is watching the bill in a CI run.

### an.live_api.LIVE_API_ENV_VAR *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'AN_LIVE_API_TESTS'*

Set this truthy to opt a run in to real, billed API calls.

### an.live_api.TRUTHY_VALUES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'1', 'on', 'true', 'yes'})*

Accepted spellings of “yes”. Anything else — including an empty string, the
shape an unset-but-exported variable takes — is “no”.

### an.live_api.live_api_enabled(env=None)

Whether this run has explicitly opted in to paid API calls.

`env` defaults to the process environment; pass a mapping to ask the
question of a hypothetical one without mutating `os.environ`.

Typed `Mapping`, not `dict`: the default IS `os.environ`, an
`os._Environ` that fails `isinstance(..., dict)`, so a `dict`
annotation was false about the function’s own primary argument and pushed
callers into copying the whole environment to satisfy it.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> live_api_enabled({})
False
>>> live_api_enabled({"ELEVEN_API_KEY": "sk-real-key"})  # a key is not consent
False
>>> live_api_enabled({LIVE_API_ENV_VAR: "1"})
True
>>> live_api_enabled({LIVE_API_ENV_VAR: "1", CI_ENV_VAR: "true"})
False
```

### an.live_api.paid_provider_key_vars()

Every environment variable a paid provider reads its API key from — the
`env.key.*` capabilities’ ([`an.capabilities.subjects.ENV_KEYS`](_autosummary/an.capabilities.subjects.html.md#an.capabilities.subjects.ENV_KEYS)), so a
new paid provider is listed once.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> {"ELEVEN_API_KEY", "ANTHROPIC_API_KEY"} <= set(paid_provider_key_vars())
True
```


# _autosummary/an.mcp.html.md

# an.mcp

The `an` MCP server: a curated, generated surface over the vocabulary and the capability registry.

ADR 0003 decision 6. The tools are plain functions in [`an.mcp.tools`](_autosummary/an.mcp.tools.html.md#module-an.mcp.tools)
(queries, typed edits, long work as start/poll jobs; the bench stays CLI-only);
[`an.mcp.server.mk_server()`](_autosummary/an.mcp.server.html.md#an.mcp.server.mk_server) projects them with `py2mcp`. The MCP stack is
an optional extra and is imported only when a server is built:

```default
pip install 'an[mcp]'
python -m an.mcp            # serve over stdio
```

`import an` never imports this package, and importing it imports no MCP
dependency (a test holds both).

```pycon
>>> from an.mcp import TOOL_REFS
>>> "an.mcp.tools:vocabulary" in TOOL_REFS
True
```

### Module Attributes

| [`TOOL_REFS`](_autosummary/an.mcp.html.md#an.mcp.TOOL_REFS)   | The tools as `module:function` references (`py2mcp.mk_mcp_from_refs`'s input).   |
|--------------------------------------------------------------|----------------------------------------------------------------------------------|

### an.mcp.TOOL_REFS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('an.mcp.tools:vocabulary', 'an.mcp.tools:vocabulary_entry', 'an.mcp.tools:scene_schema', 'an.mcp.tools:validate_scene', 'an.mcp.tools:describe_character', 'an.mcp.tools:applicable_methods', 'an.mcp.tools:why_not_method', 'an.mcp.tools:apply_patch', 'an.mcp.tools:start_render', 'an.mcp.tools:job_status')*

The tools as `module:function` references (`py2mcp.mk_mcp_from_refs`’s input).

### Modules

| [`server`](_autosummary/an.mcp.server.html.md#module-an.mcp.server)   | Build and serve the `an` MCP server with `py2mcp` (the `an[mcp]` extra).                           |
|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
| [`tools`](_autosummary/an.mcp.tools.html.md#module-an.mcp.tools)     | The curated MCP tools: plain functions over the vocabulary, the capability registry and a project. |


# _autosummary/an.mcp.server.html.md

# an.mcp.server

Build and serve the `an` MCP server with `py2mcp` (the `an[mcp]` extra).

`py2mcp` is imported inside [`mk_server()`](_autosummary/an.mcp.server.html.md#an.mcp.server.mk_server), never at module level, so this
module imports without the extra (CI collects every module’s doctests with no
extras installed) and a missing extra is a typed, install-hinting error.

### Module Attributes

| [`SERVER_INSTRUCTIONS`](_autosummary/an.mcp.server.html.md#an.mcp.server.SERVER_INSTRUCTIONS)   | What a client is told about this server (the model-facing description).   |
|------------------------------------------------------------------------|---------------------------------------------------------------------------|

### Functions

| [`main`](_autosummary/an.mcp.server.html.md#an.mcp.server.main)()                              | Serve over stdio (`python -m an.mcp`).                                                                                         |
|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| [`mk_server`](_autosummary/an.mcp.server.html.md#an.mcp.server.mk_server)(\*[, name, instructions]) | The FastMCP server with one tool per [`an.mcp.TOOL_REFS`](_autosummary/an.mcp.html.md#an.mcp.TOOL_REFS) entry. |

### Exceptions

| [`MCPExtraMissingError`](_autosummary/an.mcp.server.html.md#an.mcp.server.MCPExtraMissingError)   | The MCP server needs the `an[mcp]` extra (`py2mcp`).   |
|-------------------------------------------------------------------------|--------------------------------------------------------|

### *exception* an.mcp.server.MCPExtraMissingError

Bases: [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)

The MCP server needs the `an[mcp]` extra (`py2mcp`).

### an.mcp.server.SERVER_INSTRUCTIONS *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'Structured animation with \`an\`. Read the vocabulary (\`vocabulary\`) before writing any name into a scene: every action kind, preset, camera move, easing and method is registered and versioned, and a name outside it fails validation. Ask what a character can do (\`describe_character\`, \`applicable_methods\`, \`why_not_method\`) before requesting a method. Edit with \`apply_patch\` (dry-run first); render with \`start_render\` and poll \`job_status\`.'*

What a client is told about this server (the model-facing description).

### an.mcp.server.main()

Serve over stdio (`python -m an.mcp`).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.mcp.server.mk_server(, name='an', instructions='Structured animation with \`an\`. Read the vocabulary (\`vocabulary\`) before writing any name into a scene: every action kind, preset, camera move, easing and method is registered and versioned, and a name outside it fails validation. Ask what a character can do (\`describe_character\`, \`applicable_methods\`, \`why_not_method\`) before requesting a method. Edit with \`apply_patch\` (dry-run first); render with \`start_render\` and poll \`job_status\`.')

The FastMCP server with one tool per [`an.mcp.TOOL_REFS`](_autosummary/an.mcp.html.md#an.mcp.TOOL_REFS) entry.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)


# _autosummary/an.mcp.tools.html.md

# an.mcp.tools

The curated MCP tools: plain functions over the vocabulary, the capability registry and a project.

ADR 0003 decision 6: the MCP surface is a curated list, not all of
`_dispatch_funcs`. Each tool here is an ordinary function with simple,
JSON-shaped arguments and results (so `py2mcp` can project it, and a test
can call it without any MCP stack):

- **queries** — [`vocabulary()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.vocabulary), [`vocabulary_entry()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.vocabulary_entry),
  [`scene_schema()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.scene_schema), [`validate_scene()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.validate_scene), [`describe_character()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.describe_character),
  [`applicable_methods()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.applicable_methods), [`why_not_method()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.why_not_method);
- **edits** — [`apply_patch()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.apply_patch), a typed JSON-pointer patch (the `an
  iterate` patch shape), validated before anything is written, `dry_run` by
  default;
- **long work as jobs** — [`start_render()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.start_render) returns a job id at once and
  [`job_status()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.job_status) polls it; nothing blocks a client for minutes. The bench
  > stays CLI-only.

Every tool loads the installed genres first (explicit discovery, ADR 0001
decision 3): their presets, methods and analysers are part of the answer.

```pycon
>>> any(e["id"] == "action.tween" for e in vocabulary(kind="action"))
True
```

### Module Attributes

| [`TOOLS`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.TOOLS)   | The curated surface, in the order a client lists it.   |
|----------------------------------------------------------|--------------------------------------------------------|

### Functions

| [`applicable_methods`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.applicable_methods)(aspect, project_dir, name)   | The methods of `aspect` (`locomotion`, `speech`, …) that apply to the character.                                                            |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| [`apply_patch`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.apply_patch)(project_dir, patches[, dry_run])    | Apply typed JSON-pointer patches to the scene, validated before anything is written.                                                        |
| [`describe_character`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.describe_character)(project_dir, name)           | What a character affords, and per aspect the default method, the applicable ones, and the missing capabilities (with remedies) of the rest. |
| [`job_status`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.job_status)(job)                                 | A job's state: `running`, `done` (with its `result`) or `failed` (with its `error`).                                                        |
| [`scene_schema`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.scene_schema)([document])                        | The JSON Schema of a document: `scene` (the scene IR) or `character` (a descriptor).                                                        |
| [`start_render`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.start_render)(project_dir[, strict_assets, ...]) | Start rendering the project to an mp4; returns `{job}` at once — poll [`job_status()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.job_status).        |
| [`validate_scene`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.validate_scene)(project_dir)                     | Validate the project's scene (schema and semantics): `{passed, findings}`.                                                                  |
| [`vocabulary`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.vocabulary)([kind, owner])                       | Every registered vocabulary entry (presets, kinds, easings, camera moves, methods, IR fields).                                              |
| [`vocabulary_entry`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.vocabulary_entry)(entry_id)                      | One vocabulary entry by id (`motion.walk`, `loco.legged_cycle`).                                                                            |
| [`why_not_method`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.why_not_method)(method, project_dir, name)       | What the character lacks for `method` to apply, each with its remedy (empty: it applies).                                                   |

### an.mcp.tools.TOOLS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)* *= (<function vocabulary>, <function vocabulary_entry>, <function scene_schema>, <function validate_scene>, <function describe_character>, <function applicable_methods>, <function why_not_method>, <function apply_patch>, <function start_render>, <function job_status>)*

The curated surface, in the order a client lists it.

### an.mcp.tools.applicable_methods(aspect, project_dir, name)

The methods of `aspect` (`locomotion`, `speech`, …) that apply to the character.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.mcp.tools.apply_patch(project_dir, patches, dry_run=True)

Apply typed JSON-pointer patches to the scene, validated before anything is written.

`patches` are `{op: set|append|delete, path: "timeline/0/duration",
value: …}` (the `an iterate` patch shape). The patched scene is validated
(schema and semantics); with `dry_run=False` and a passing validation it
is saved through the scenes store (`scene.md` and `ir/scene.json` stay
in step) and the edit is recorded in the decisions log.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.mcp.tools.describe_character(project_dir, name)

What a character affords, and per aspect the default method, the applicable
ones, and the missing capabilities (with remedies) of the rest.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.mcp.tools.job_status(job)

A job’s state: `running`, `done` (with its `result`) or `failed` (with its `error`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.mcp.tools.scene_schema(document='scene')

The JSON Schema of a document: `scene` (the scene IR) or `character` (a descriptor).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.mcp.tools.start_render(project_dir, strict_assets=False, output_name='main')

Start rendering the project to an mp4; returns `{job}` at once — poll [`job_status()`](_autosummary/an.mcp.tools.html.md#an.mcp.tools.job_status).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.mcp.tools.validate_scene(project_dir)

Validate the project’s scene (schema and semantics): `{passed, findings}`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.mcp.tools.vocabulary(kind='', owner='')

Every registered vocabulary entry (presets, kinds, easings, camera moves, methods, IR fields).

Each entry: id, kind, name (how a document spells it), version, title,
description, accepted spectrum levels, params (JSON Schema with defaults),
examples, requires. `kind` filters (`motion_preset`, `method`, …),
`owner` filters by who registered it (`an`, a genre).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.mcp.tools.vocabulary_entry(entry_id)

One vocabulary entry by id (`motion.walk`, `loco.legged_cycle`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.mcp.tools.why_not_method(method, project_dir, name)

What the character lacks for `method` to apply, each with its remedy (empty: it applies).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]


# _autosummary/an.measurements.html.md

# an.measurements

Measured durations: shots whose renderer, not their author, decides their length.

A whole-shot renderer that **owns its clock** (Manim: only the scene file’s own
`play` and `wait` calls fix its length) implements `measure_duration`
(`ClockOwningRenderer`). Its answer is DERIVED data:

- it lives in a derived store keyed by the shot’s content (the renderer’s
  `measurements` store), never in `scene.md` or `ir/scene.json` — a render
  never rewrites what the author wrote (an#279 review, H1);
- [`settle_durations()`](_autosummary/an.measurements.html.md#an.measurements.settle_durations) applies it IN MEMORY, to a copy of the scene, before
  anything reads `shot.duration` — the film timeline, captions, the sound
  layer, the cache keys — so the film’s layout is a pure function of the IR
  and the measurements;
- `an sync --accept-measured` ([`accept_measured()`](_autosummary/an.measurements.html.md#an.measurements.accept_measured)) takes it into the
  authored scene on request, patching each shot’s `duration:` line in place.

This is how core study §4.2’s “written back into the IR” is resolved: the IR the
render LAYS OUT holds the measured length; the IR the author OWNS is untouched
unless they accept it.

**Narration longer than the picture is never cut silently.** A shot’s settled
length is `max(measured, end of its dialogue)`: the renderer holds its last
frame for the difference, and a warning says so — or, under `strict`
(`--strict-assets`, which already refuses stand-ins), the render is refused.

```pycon
>>> declared_duration(Shot(id="a", renderer="manim")) is None   # the schema's placeholder
True
>>> declared_duration(Shot(id="a", renderer="manim", duration=3.0))
3.0
```

### Functions

| [`accept_measured`](_autosummary/an.measurements.html.md#an.measurements.accept_measured)(project_dir)                    | Write each clock-owned shot's MEASURED length into the authored scene.                                      |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| [`clock_owner_for`](_autosummary/an.measurements.html.md#an.measurements.clock_owner_for)(shot[, registry])               | The registered renderer of `shot` if it owns its clock, else `None`.                                        |
| [`declared_duration`](_autosummary/an.measurements.html.md#an.measurements.declared_duration)(shot)                         | The duration the AUTHOR wrote, or `None`.                                                                   |
| [`settle_durations`](_autosummary/an.measurements.html.md#an.measurements.settle_durations)(scene, ctx, \*, render[, ...]) | A COPY of `scene` whose clock-owned shots carry their settled length, and the findings about them.          |
| [`warn_findings`](_autosummary/an.measurements.html.md#an.measurements.warn_findings)(findings, \*[, stacklevel])       | Warn each finding as a [`ShotFindingWarning`](_autosummary/an.measurements.html.md#an.measurements.ShotFindingWarning) (`location: …`). |

### Exceptions

| [`MeasurementError`](_autosummary/an.measurements.html.md#an.measurements.MeasurementError)   | A measured shot cannot be laid out as asked (e.g. a hold under strict mode).                                                           |
|---------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| [`ShotFindingWarning`](_autosummary/an.measurements.html.md#an.measurements.ShotFindingWarning) | A finding about a shot, warned by `an render` — located by `file:line` when the thing to fix is an opaque source (a Manim scene file). |

### *exception* an.measurements.MeasurementError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A measured shot cannot be laid out as asked (e.g. a hold under strict mode).

### *exception* an.measurements.ShotFindingWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A finding about a shot, warned by `an render` — located by `file:line`
when the thing to fix is an opaque source (a Manim scene file).

### an.measurements.accept_measured(project_dir)

Write each clock-owned shot’s MEASURED length into the authored scene.

Only stored measurements (render first); only shots whose scene duration
differs. Each `duration:` line is patched in place through
[`an.stores.scenes.ScenesStore.patch_shot_durations()`](_autosummary/an.stores.scenes.html.md#an.stores.scenes.ScenesStore.patch_shot_durations), so the prose and
comments of `scene.md` are kept. Returns `{shot id: seconds}` written.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.measurements.clock_owner_for(shot, registry=None)

The registered renderer of `shot` if it owns its clock, else `None`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.measurements.declared_duration(shot)

The duration the AUTHOR wrote, or `None`.

`scene.md` fills the schema’s placeholder (`an.base.DEFAULT_DURATION`)
into a shot that writes no `duration:`, so that value reads as “not
declared” — an explicit `duration: 5` is indistinguishable from it.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.measurements.settle_durations(scene, ctx, , render, force=False, strict=False, registry=None)

A COPY of `scene` whose clock-owned shots carry their settled length,
and the findings about them. `scene` itself is never modified.

`render=False` (`an validate`) uses stored measurements only: a shot
never measured is reported as “length unknown until rendered” and judged
against nothing. `render=True` (`an render`) measures what is not
stored; `force` measures everything afresh.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/an.verify.html.md#an.verify.Finding)]]

### an.measurements.warn_findings(findings, , stacklevel=3)

Warn each finding as a [`ShotFindingWarning`](_autosummary/an.measurements.html.md#an.measurements.ShotFindingWarning) (`location: …`).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.media.frames.html.md

# an.media.frames

The frame directory every engine writes and every sink reads.

One PNG per output frame, named by [`DEFAULT_FRAME_PNG_PATTERN`](_autosummary/an.media.frames.html.md#an.media.frames.DEFAULT_FRAME_PNG_PATTERN), at the
DECLARED size: the frame stage (`an.engines.capture`) resolves supersampling
and the open shutter before a file is written, so nothing downstream ever reads
a resolution off a file or averages anything. The bench, the golden gate, film
assembly and every sink rely on exactly that.

Moved from `an/adapters/cutout/render.py` (an#247); the old name still reads
from here.

```pycon
>>> from pathlib import Path
>>> frame_path(Path("frames"), 7).name
'frame_000007.png'
```

### Module Attributes

| [`DEFAULT_FRAME_PNG_PATTERN`](_autosummary/an.media.frames.html.md#an.media.frames.DEFAULT_FRAME_PNG_PATTERN)   | The printf pattern of a frame file.   |
|------------------------------------------------------------------------------|---------------------------------------|

### Functions

| [`frame_path`](_autosummary/an.media.frames.html.md#an.media.frames.frame_path)(frames_dir, index)            | Where frame `index` lives in `frames_dir`.                          |
|-------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
| [`missing_frames`](_autosummary/an.media.frames.html.md#an.media.frames.missing_frames)(frames_dir, total_frames) | The frame indices in `[0, total_frames)` that have no file on disk. |

### an.media.frames.DEFAULT_FRAME_PNG_PATTERN *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'frame_%06d.png'*

The printf pattern of a frame file. ffmpeg reads the same pattern, so a
rename here is a rename of every mux’s input argument too.

### an.media.frames.frame_path(frames_dir, index)

Where frame `index` lives in `frames_dir`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.media.frames.missing_frames(frames_dir, total_frames)

The frame indices in `[0, total_frames)` that have no file on disk.

Checked on DISK rather than on a capture loop’s bookkeeping, because the
mux reads the directory: the directory is what must hold every frame.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> import tempfile
>>> d = Path(tempfile.mkdtemp())
>>> _ = frame_path(d, 0).write_bytes(b"")
>>> missing_frames(d, 3)
[1, 2]
```


# _autosummary/an.media.gif.html.md

# an.media.gif

The GIF sink: ONE palette recipe, for flat 2D art.

The recipe the demo gallery has shipped since it existed, moved here from
`misc/demos/build_demos.py` (an#247) so there is one copy of it: the clip’s
own palette (`palettegen`), applied with **no dithering**, at a reduced rate
and width, with nearest-neighbour scaling. The same lessons were learned three
times across the fleet (`walkthru`’s palette graph, `previz`’s `gifenc`
sink); this is `an`’s copy, and the only one in this repository.

- `dither=none` – dithering a flat fill invents texture that is not in the
  render.
- a limited palette – these frames genuinely hold few colours.
- 12 fps rather than the render’s 24 – a GIF stores whole frames, so halving
  the rate halves the file; nothing in a short clip at gallery size moves fast
  enough for the drop to read. Keep the mp4 beside it for the full rate.

```pycon
>>> gif_filter()
'fps=12,scale=480:-1:flags=neighbor,split[a][b];[a]palettegen=max_colors=128[p];[b][p]paletteuse=dither=none'
>>> gif_filter(crop="200:200:0:0").startswith('crop=200:200:0:0,fps=12')
True
```

### Module Attributes

| [`GIF_FPS`](_autosummary/an.media.gif.html.md#an.media.gif.GIF_FPS)         | Output rate.                                                 |
|------------------------------------------------------------------|--------------------------------------------------------------|
| [`GIF_WIDTH`](_autosummary/an.media.gif.html.md#an.media.gif.GIF_WIDTH)       | Output width in pixels; the height follows the aspect ratio. |
| [`GIF_MAX_COLOURS`](_autosummary/an.media.gif.html.md#an.media.gif.GIF_MAX_COLOURS) | Palette size.                                                |

### Functions

| [`gif_filter`](_autosummary/an.media.gif.html.md#an.media.gif.gif_filter)(\*[, crop, fps, width, max_colours])   | The ffmpeg filter graph of the recipe.                                |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`to_gif`](_autosummary/an.media.gif.html.md#an.media.gif.to_gif)(source, gif, \*[, crop, source_fps, ...])  | `source` -> GIF with a palette generated from the clip's own colours. |

### an.media.gif.GIF_FPS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 12*

Output rate. See the module docstring for why it is half the render’s.

### an.media.gif.GIF_MAX_COLOURS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 128*

Palette size. Flat art holds few colours; more buys nothing visible.

### an.media.gif.GIF_WIDTH *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 480*

Output width in pixels; the height follows the aspect ratio.

### an.media.gif.gif_filter(, crop='', fps=12, width=480, max_colours=128)

The ffmpeg filter graph of the recipe. `crop` is an ffmpeg `crop` expression.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.media.gif.to_gif(source, gif, , crop='', source_fps=None, fps=12, width=480, max_colours=128)

`source` -> GIF with a palette generated from the clip’s own colours.

`source` is a video file (an mp4 the renderer delivered) or a frame
DIRECTORY ([`an.media.frames`](_autosummary/an.media.frames.html.md#module-an.media.frames)), in which case `source_fps` – the
rate the frames were rendered at – is required.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.media.grid.html.md

# an.media.grid

Tile images into one contact sheet: [`tile()`](_autosummary/an.media.grid.html.md#an.media.grid.tile) (an#347).

A small pure function over PNG bytes — decoded and encoded by
[`an.bench.png`](_autosummary/an.bench.png.html.md#module-an.bench.png) (numpy and the standard library), each image fitted into a
square cell without distortion, an optional caption under each. `an probe`
tiles several instants of a shot with it and `an library sheet` one specimen
per asset.

Candidate fleet component: one production wrote three ad hoc copies of this
before it existed (design of an#331, §5.3).

```pycon
>>> from an.bench.png import decode_png, encode_png
>>> import numpy as np
>>> red = encode_png(np.full((4, 8, 3), (255, 0, 0), np.uint8))
>>> sheet = decode_png(tile([red, red, red], cell=16, columns=2))
>>> sheet.shape
(44, 44, 3)
```

### Functions

| [`tile`](_autosummary/an.media.grid.html.md#an.media.grid.tile)(images, \*[, cell, columns, labels, ...])   | One PNG holding every image of `images` (PNG bytes), each in a `cell` square.                                                                                                                         |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`trim`](_autosummary/an.media.grid.html.md#an.media.grid.trim)(image, \*[, tolerance, margin])             | `image` (PNG bytes) cropped to what differs from its corner colour, plus a margin.                                                                                                                    |
| [`fit_caption`](_autosummary/an.media.grid.html.md#an.media.grid.fit_caption)(text, room, measure)                 | The caption that fits `room` (in `measure`'s units): a long one is shortened with `…`; a `(head, tail)` pair keeps its tail whole and shortens only the head (an#460), unless not even the tail fits. |

### an.media.grid.fit_caption(text, room, measure)

The caption that fits `room` (in `measure`’s units): a long one is
shortened with `…`; a `(head, tail)` pair keeps its tail whole and
shortens only the head (an#460), unless not even the tail fits.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> fit_caption(("cutan:prop.desk-oversimplified", " [free]"), 20, len)
'cutan:prop.d… [free]'
>>> fit_caption("cutan:prop.desk-oversimplified [free]", 20, len)
'cutan:prop.desk-ove…'
```

### an.media.grid.tile(images, , cell=256, columns=None, labels=None, gap=4, background=(255, 255, 255))

One PNG holding every image of `images` (PNG bytes), each in a `cell` square.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

columns: cells per row (default: the smallest square grid that holds them)
labels: one caption per image, drawn under its cell; a `(head, tail)`

> pair is shortened in its head only, so the tail always shows

### an.media.grid.trim(image, , tolerance=8, margin=0.06)

`image` (PNG bytes) cropped to what differs from its corner colour, plus a margin.

An image that is all background is returned unchanged.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> import numpy as np
>>> a = np.full((20, 20, 3), 255, np.uint8); a[5:9, 10:12] = 0
>>> decode_png(trim(encode_png(a), margin=0)).shape
(4, 2, 3)
```


# _autosummary/an.media.html.md

# an.media

Frames to deliverables, engine-independent: the frame stage’s resolves and the sinks.

Core (ADR 0001 decisions 8(a) and 12; core study §2.8 and §5). Nothing here
knows what drew a frame: the stage engine, a Manim shot or a `burns` crop all
hand over PNGs, and everything below is the same for each.

- [`an.media.frames`](_autosummary/an.media.frames.html.md#module-an.media.frames) – the frame directory’s naming (`frame_%06d.png`).
- [`an.media.supersample`](_autosummary/an.media.supersample.html.md#module-an.media.supersample) – the SPATIAL resolve: an exact `k x k` block mean.
- [`an.media.shutter`](_autosummary/an.media.shutter.html.md#module-an.media.shutter) – the TEMPORAL resolve: a frame’s sample instants averaged.
- [`an.media.mp4`](_autosummary/an.media.mp4.html.md#module-an.media.mp4) – the MP4 sink with the pinned argv, plus the shot’s audio mux.
- [`an.media.gif`](_autosummary/an.media.gif.html.md#module-an.media.gif) – the GIF sink’s one palette recipe.
- [`an.media.sinks`](_autosummary/an.media.sinks.html.md#module-an.media.sinks) – the `FrameSink` protocol and the name-keyed sink registry.

Moved out of `an/adapters/cutout/` and `misc/demos/build_demos.py` in an#247;
the old paths re-export (and the module globals the bench rebinds stay LIVE at
the old paths, see `an._shims`). Film assembly (`an.assemble`) consumes
these sinks’ output and stays where it is.

```pycon
>>> from an.media import get_sink
>>> get_sink("mp4").suffix
'.mp4'
```

### Functions

| [`block_mean_resolve`](_autosummary/an.media.html.md#an.media.block_mean_resolve)(frame, factor)           | `(H*k, W*k, C)` uint8 -> `(H, W, C)` uint8, by an exact `k x k` mean.       |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`check_factor`](_autosummary/an.media.html.md#an.media.check_factor)(factor)                        | Validate a supersample factor, or refuse with the reason.                   |
| [`check_frame_samples`](_autosummary/an.media.html.md#an.media.check_frame_samples)(frame_samples, \*, ...) | Validate `frame_samples` against the render it is for; normalise to tuples. |
| [`frame_path`](_autosummary/an.media.html.md#an.media.frame_path)(frames_dir, index)               | Where frame `index` lives in `frames_dir`.                                  |
| [`get_sink`](_autosummary/an.media.html.md#an.media.get_sink)(name, \*\*knobs)                   | A sink by name, built with `knobs`; the error lists what exists.            |
| [`mean_png_bytes`](_autosummary/an.media.html.md#an.media.mean_png_bytes)(shots, \*, factor)           | Spatially resolve each screenshot by `factor`, then average them in time.   |
| [`missing_frames`](_autosummary/an.media.html.md#an.media.missing_frames)(frames_dir, total_frames)    | The frame indices in `[0, total_frames)` that have no file on disk.         |
| [`register_sink`](_autosummary/an.media.html.md#an.media.register_sink)(name, factory, \*[, replace]) | Register a sink factory under `name`; refuse a silent replacement.          |
| [`sink_names`](_autosummary/an.media.html.md#an.media.sink_names)()                                | The registered sink names.                                                  |
| [`temporal_mean`](_autosummary/an.media.html.md#an.media.temporal_mean)(frames)                       | `k` equal-shape uint8 frames -> their exact per-pixel mean, uint8.          |

### Classes

| [`FrameSink`](_autosummary/an.media.html.md#an.media.FrameSink)(\*args, \*\*kwargs)                 | Writes a frame directory as one deliverable.                                                                                                         |
|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`GifSink`](_autosummary/an.media.html.md#an.media.GifSink)([crop, fps, width, max_colours, ...]) | The demo gallery's palette recipe.                                                                                                                   |
| [`Mp4Sink`](_autosummary/an.media.html.md#an.media.Mp4Sink)([pix_fmt, name, suffix])              | The silent picture mux (the shot's audio is laid by [`an.media.mp4.mux_shot()`](_autosummary/an.media.mp4.html.md#an.media.mp4.mux_shot)). |
| [`PngSequenceSink`](_autosummary/an.media.html.md#an.media.PngSequenceSink)([name, suffix])               | The frames themselves, renumbered from 0 into `output` (a directory).                                                                                |

### Exceptions

| [`MediaError`](_autosummary/an.media.html.md#an.media.MediaError)   | A sink could not write what it was asked to.   |
|---------------------------------------------------------------|------------------------------------------------|

### *class* an.media.FrameSink(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Writes a frame directory as one deliverable.

#### name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The registry name, and the format’s usual name.

#### suffix *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The deliverable’s suffix (`".mp4"`), or `""` for a directory.

#### write(frames_dir, output, , fps)

Write `frames_dir` (rendered at `fps`) to `output`; return it.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### *class* an.media.GifSink(crop='', fps=12, width=480, max_colours=128, name='gif', suffix='.gif')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The demo gallery’s palette recipe.

### *exception* an.media.MediaError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A sink could not write what it was asked to. Carries actionable detail.

A renderer re-raises it as its own typed error at its boundary
(`frame_stage_renderer(..., error=...)`), so a cut-out render still fails
with `CutoutRenderError`.

### *class* an.media.Mp4Sink(pix_fmt=None, name='mp4', suffix='.mp4')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The silent picture mux (the shot’s audio is laid by [`an.media.mp4.mux_shot()`](_autosummary/an.media.mp4.html.md#an.media.mp4.mux_shot)).

`pix_fmt=None` is the module default AT CALL TIME, the seam the bench pulls.

### *class* an.media.PngSequenceSink(name='png', suffix='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The frames themselves, renumbered from 0 into `output` (a directory).

```pycon
>>> import tempfile
>>> src, out = Path(tempfile.mkdtemp()), Path(tempfile.mkdtemp()) / "seq"
>>> _ = frame_path(src, 0).write_bytes(b"png")
>>> sorted(p.name for p in PngSequenceSink().write(src, out, fps=24).iterdir())
['frame_000000.png']
```

### an.media.block_mean_resolve(frame, factor)

`(H*k, W*k, C)` uint8 -> `(H, W, C)` uint8, by an exact `k x k` mean.

Two-step, summing rows and then columns in `uint16`, rather than the
obvious `reshape(...).astype(float64).mean(axis=(1, 3))`. The two agree
**bit for bit** — asserted exhaustively over every possible 2x2 block, and
on real frames — and the two-step form is 2.3x faster at 1080p (111.7 ms
against 262.0 ms), because the cost here is the strided reduce and the
64-bit temporary, not the arithmetic.

Rounding is spelled out rather than inherited: `np.rint` is banker’s
rounding, so a block averaging exactly `.5` goes to the EVEN neighbour.
Getting that wrong changes one code value on every half-block, which is
invisible in a picture and moves every golden.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> f = np.array([[0, 0, 1, 2], [0, 4, 1, 2]], np.uint8)[..., None].repeat(3, -1)
>>> block_mean_resolve(f, 2)[0, :, 0].tolist()
[1, 2]
>>> block_mean_resolve(f, 1) is f
True
```

### an.media.check_factor(factor)

Validate a supersample factor, or refuse with the reason.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> check_factor(1), check_factor(2)
(1, 2)
>>> check_factor(0)
Traceback (most recent call last):
  ...
an.media.supersample.SupersampleError: supersample must be >= 1, got 0
```

### an.media.check_frame_samples(frame_samples, , total_frames, duration)

Validate `frame_samples` against the render it is for; normalise to tuples.

Checked before a browser launches, for `check_factor`’s reason: the render
costs minutes and this costs microseconds.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> check_frame_samples(None, total_frames=3, duration=0.1) is None
True
>>> check_frame_samples([[0.0], [0.5, 0.6]], total_frames=2, duration=1.0)
((0.0,), (0.5, 0.6))
>>> check_frame_samples([[0.0]], total_frames=2, duration=1.0)
Traceback (most recent call last):
  ...
an.media.shutter.ShutterError: frame_samples has 1 frame(s) but this render has 2; a frame clock must describe every frame, and only those
```

### an.media.frame_path(frames_dir, index)

Where frame `index` lives in `frames_dir`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.media.get_sink(name, \*\*knobs)

A sink by name, built with `knobs`; the error lists what exists.

* **Return type:**
  [`FrameSink`](_autosummary/an.media.sinks.html.md#an.media.sinks.FrameSink)

### an.media.mean_png_bytes(shots, , factor)

Spatially resolve each screenshot by `factor`, then average them in time.

One screenshot takes exactly the spatial path — the same bytes a render
without a shutter writes.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### an.media.missing_frames(frames_dir, total_frames)

The frame indices in `[0, total_frames)` that have no file on disk.

Checked on DISK rather than on a capture loop’s bookkeeping, because the
mux reads the directory: the directory is what must hold every frame.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> import tempfile
>>> d = Path(tempfile.mkdtemp())
>>> _ = frame_path(d, 0).write_bytes(b"")
>>> missing_frames(d, 3)
[1, 2]
```

### an.media.register_sink(name, factory, , replace=False)

Register a sink factory under `name`; refuse a silent replacement.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.media.sink_names()

The registered sink names.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.media.temporal_mean(frames)

`k` equal-shape uint8 frames -> their exact per-pixel mean, uint8.

Rounded half-to-even, the rule `block_mean_resolve` spells out, so the
temporal and spatial resolves agree about every tie. The mean is of the
ENCODED (sRGB) values, as the spatial resolve’s is — not of linear light, so
a smear’s profile is not exactly a physical sensor’s. The centroid of what
is drawn is still the average of the sampled positions, which is the
property the impact ground truth relies on.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> a, b = np.full((1, 2, 3), 1, np.uint8), np.full((1, 2, 3), 2, np.uint8)
>>> temporal_mean([a, b])[0, 0].tolist()   # 1.5 -> 2 (even)
[2, 2, 2]
>>> temporal_mean([a, a + 2])[0, 0].tolist()   # 2.0 exactly
[2, 2, 2]
>>> temporal_mean([a]) is a
True
```

### Modules

| [`frames`](_autosummary/an.media.frames.html.md#module-an.media.frames)           | The frame directory every engine writes and every sink reads.                  |
|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`gif`](_autosummary/an.media.gif.html.md#module-an.media.gif)                 | The GIF sink: ONE palette recipe, for flat 2D art.                             |
| [`grid`](_autosummary/an.media.grid.html.md#module-an.media.grid)               | Tile images into one contact sheet: `tile()` (an#347).                         |
| [`mp4`](_autosummary/an.media.mp4.html.md#module-an.media.mp4)                 | The MP4 sink: PNG frames -> the delivered H.264 mp4, with the pinned argv.     |
| [`shutter`](_autosummary/an.media.shutter.html.md#module-an.media.shutter)         | The temporal half of the frame stage: average several instants into one frame. |
| [`sinks`](_autosummary/an.media.sinks.html.md#module-an.media.sinks)             | Frame sinks: a frame directory in, one deliverable out -- one sink per format. |
| [`supersample`](_autosummary/an.media.supersample.html.md#module-an.media.supersample) | Render bigger, then resolve back exactly — the supersample knob's two halves.  |


# _autosummary/an.media.mp4.html.md

# an.media.mp4

The MP4 sink: PNG frames -> the delivered H.264 mp4, with the pinned argv.

Every flag here has a measured reason, recorded beside it; the
`an-dev-render-pipeline` skill (section 3) is the table of them. The argv is
pinned EXACTLY (`tests/test_encode_pins.py` compares it for equality, not as a
subset), because `DETERMINISTIC_X264_ARGS` is a comparability key of every
encode-side metric in the bench’s ledger.

Engine-independent: it reads a frame directory ([`an.media.frames`](_autosummary/an.media.frames.html.md#module-an.media.frames)) and
knows nothing about what drew the frames. Moved from
`an/adapters/cutout/render.py` (an#247), whose old names (`_ffmpeg_mux`,
`_check_pix_fmt`, `DEFAULT_PIX_FMT`, …) are LIVE aliases of these: reading
one reads here, and rebinding one – the bench’s `high_crf` lever, a test’s
`patch_subprocess_run` – rebinds here (`an._shims`).

**Two seams are module globals read at CALL time, deliberately**:
`DETERMINISTIC_X264_ARGS` (the `high_crf` lever rebinds it) and
`DEFAULT_PIX_FMT`. Hoisting either into a default argument binds it at
`def` time and disarms the lever silently.

```pycon
>>> check_pix_fmt(None), check_pix_fmt("yuv444p")
('yuv420p', 'yuv444p')
```

### Module Attributes

| [`AUDIO_SAMPLE_RATE`](_autosummary/an.media.mp4.html.md#an.media.mp4.AUDIO_SAMPLE_RATE)       | The sample rate of every shot's audio stream, silent base included.                                                                           |
|--------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| [`DEFAULT_PIX_FMT`](_autosummary/an.media.mp4.html.md#an.media.mp4.DEFAULT_PIX_FMT)         | The delivered encode's pixel format, and \*\*the one first-order quality lever in this file\*\*.                                              |
| [`SUPPORTED_PIX_FMTS`](_autosummary/an.media.mp4.html.md#an.media.mp4.SUPPORTED_PIX_FMTS)      | a typo would reach ffmpeg as an obscure failure minutes into a render, and a format outside this set has not been measured against the panel. |
| [`DETERMINISTIC_X264_ARGS`](_autosummary/an.media.mp4.html.md#an.media.mp4.DETERMINISTIC_X264_ARGS) | x264 encode knobs pinned so the delivered mp4 is a function of the frames rather than of the machine (an#34, research §2).                    |

### Functions

| [`add_audio`](_autosummary/an.media.mp4.html.md#an.media.mp4.add_audio)(video_path, audio_inputs, ...)       | Mux a silence base + `audio_inputs` (path, delay_s) onto `video_path`.       |
|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`check_pix_fmt`](_autosummary/an.media.mp4.html.md#an.media.mp4.check_pix_fmt)(pix_fmt)                         | Resolve and validate the pixel format, or refuse with the whole list.        |
| [`ensure_ffmpeg`](_autosummary/an.media.mp4.html.md#an.media.mp4.ensure_ffmpeg)()                                | Refuse before anything launches when ffmpeg is not on `PATH`.                |
| [`mux_frames`](_autosummary/an.media.mp4.html.md#an.media.mp4.mux_frames)(frames_dir, fps, output_mp4[, ...]) | Mux a PNG sequence to H.264 mp4.                                             |
| [`mux_shot`](_autosummary/an.media.mp4.html.md#an.media.mp4.mux_shot)(frames_dir, shot, ctx, work_dir, ...) | Frames → the delivered per-shot mp4: a silent mux, then the audio mux.       |
| [`stage_audio_inputs`](_autosummary/an.media.mp4.html.md#an.media.mp4.stage_audio_inputs)(shot, ctx, work_dir)        | Write the shot's per-dialogue audio bytes to disk and return (path, delay)s. |

### Exceptions

| [`MediaError`](_autosummary/an.media.mp4.html.md#an.media.mp4.MediaError)   | A sink could not write what it was asked to.   |
|---------------------------------------------------------------|------------------------------------------------|

### an.media.mp4.AUDIO_SAMPLE_RATE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 44100*

The sample rate of every shot’s audio stream, silent base included. One
rate for the whole film, so the concat never resamples.

### an.media.mp4.DEFAULT_PIX_FMT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'yuv420p'*

The delivered encode’s pixel format, and \*\*the one first-order quality lever
in this file\*\*. Measured on 30 real 1080p `an` frames, edge-band mean error:
current flags 11.35, crf18 4:2:0 11.05, crf18 `-tune animation` 10.96,
mathematically lossless 4:2:0 **10.15** — and crf18 **4:4:4 3.79**.
Losslessness buys 8%; dropping chroma subsampling buys **66%**. Wave 2’s own
conclusion: “bitrate is second-order, pixel format is first-order”.

\*\*The default stays 4:2:0 because that is a PRODUCT constraint, not an
encoder-tuning one.\*\* High 4:4:4 Predictive is refused by many hardware
decoders, browsers and platforms, so flipping it would hand a design partner
a file they cannot play. 4:4:4 is reachable per render
(`an render --pix-fmt yuv444p`), which is the right shape for a knob whose
right answer depends on where the file is going.

Read as a MODULE GLOBAL at call time, deliberately: that is what lets the
bench’s lever rebind it from outside, exactly as `high_crf` rebinds
`DETERMINISTIC_X264_ARGS`. Hoisting either into a default argument binds it
at `def` time and disarms the lever silently.

### an.media.mp4.DETERMINISTIC_X264_ARGS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('-threads', '1', '-crf', '23', '-preset', 'medium', '-colorspace', 'bt709', '-color_primaries', 'bt709', '-color_trc', 'bt709', '-color_range', 'tv', '-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709')*

x264 encode knobs pinned so the delivered mp4 is a function of the frames
rather than of the machine (an#34, research §2).

`-threads 1` — `-threads 1/4/11` all give bit-identical decoded pixels, so
this looks unnecessary on a laptop. It is not: `auto` raises
`lookahead_threads` above 1 at roughly `-threads >= 12`, and a forced
`lookahead-threads=4` changes 86.2% of the bytes (max delta 80). A big CI
runner crosses that line and a 4-core dev box never will, which is precisely
how an unpinned thread count ships without anyone seeing it.

`-crf 23 -preset medium` — both are libx264’s compiled-in defaults today, so
passing them changes nothing now and pins us against a build whose defaults
differ. Worth pinning because preset swings distinct colour counts \*\*2.3x,
non-monotonically\*\* (ultrafast 3141, veryfast 7296, medium 6064, slower 5393)
against a crf18->23 signal of 1.35x — an unpinned preset dominates the very
signal a quality ledger tries to measure.

BT.709 is the one knob here that CHANGES today’s output, and it changes more
than the research predicted — measured, not assumed (an#34):

- `-colorspace bt709` does not merely *tag* the file. It sets the matrix of
  the auto-inserted RGB->YUV conversion, so the \*\*encoded luma and chroma
  planes themselves change\*\*. Confirmed by construction: forcing
  `scale=out_color_matrix=bt601` reproduces the untagged output’s decoded
  stream byte-for-byte, i.e. `an` has been converting with BT.601 all along.
  **On ffmpeg 8/9. It is false on ffmpeg 6.1** — where the same flags reach
  only the VUI and the planes stay BT.601 (an#148, measured; see
  `an.base.BT709_SCALE_FILTER` for the numbers). That is why the mux now
  states the conversion explicitly with `-vf` instead of inferring it from
  these flags, which stay for the tag they land.
- `-color_range tv` is a **no-op today** (limited range is already the
  default for yuv420p here). Pinned anyway, so a build that defaults
  differently cannot change the output silently.
- The ffmpeg-level `-color_primaries` / `-color_trc` flags \*\*do not reach the
  bitstream\*\*: with them alone, ffprobe reports `color_space=bt709` and
  `color_primaries=unknown`, `color_transfer=unknown`. `-x264-params` is what
  lands all three in the VUI, and it leaves the decoded stream identical. A
  half-tagged file is worse than an untagged one — the player stops guessing
  the matrix but still guesses the primaries.

Why bother: untagged, the *player* picks its matrix by a height heuristic
(BT.601 below ~576 lines). Every shipped `an` example is 320x240 to 640x360,
so encode and playback agree by luck; at 1080p the same code would encode
with BT.601 and be displayed as BT.709, a silent, resolution-dependent colour
error. Pinning both sides to BT.709 makes them agree at every resolution.
This is a **one-time deliberate re-baseline** of every mp4 — cheap now,
because no ledger exists yet to invalidate.

### *exception* an.media.mp4.MediaError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A sink could not write what it was asked to. Carries actionable detail.

A renderer re-raises it as its own typed error at its boundary
(`frame_stage_renderer(..., error=...)`), so a cut-out render still fails
with `CutoutRenderError`.

### an.media.mp4.SUPPORTED_PIX_FMTS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('yuv420p', 'yuv444p')*

a typo would reach ffmpeg
as an obscure failure minutes into a render, and a format outside this set
has not been measured against the panel.

* **Type:**
  The formats the knob accepts. Not an open string

### an.media.mp4.add_audio(video_path, audio_inputs, output_path, duration_s)

Mux a silence base + `audio_inputs` (path, delay_s) onto `video_path`.

Always emits an audio stream. `anullsrc` provides the silent base track
of length `duration_s` so concat across shots is safe; dialogue lines
are overlaid via `adelay` + `amix`. `duration_s` must be the
picture’s length (frames / fps): the concat advances each shot by its
container length, which is the longer of the two streams (an#195).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.media.mp4.check_pix_fmt(pix_fmt)

Resolve and validate the pixel format, or refuse with the whole list.

`None` resolves to [`DEFAULT_PIX_FMT`](_autosummary/an.media.mp4.html.md#an.media.mp4.DEFAULT_PIX_FMT) **at call time**, which is what
lets the bench’s lever rebind the module global and reach this render.

Refuses an unknown format rather than passing it to ffmpeg: a typo would
otherwise surface minutes into a render as an obscure encoder error, and on
the second shot of a parallel render it would surface from a thread. A
format outside the list has also never been measured against the panel.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.media.mp4.ensure_ffmpeg()

Refuse before anything launches when ffmpeg is not on `PATH`.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.media.mp4.mux_frames(frames_dir, fps, output_mp4, pix_fmt=None)

Mux a PNG sequence to H.264 mp4.

`pix_fmt=None` means “whatever the module default is **right now**”, which
is what keeps the bench’s `pix_fmt` lever able to reach this call by
rebinding [`DEFAULT_PIX_FMT`](_autosummary/an.media.mp4.html.md#an.media.mp4.DEFAULT_PIX_FMT). A caller that passes one wins; the bench
never passes one, so the lever reaches the encode AND the recorded
environment, and the two cannot disagree.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.media.mp4.mux_shot(frames_dir, shot, ctx, work_dir, output_mp4, , n_frames, pix_fmt=None)

Frames → the delivered per-shot mp4: a silent mux, then the audio mux.
Returns how many dialogue audio tracks were laid under the picture.

Every shot mp4 carries an AAC stream (silent if no dialogue) so the final
ffmpeg concat across heterogeneous shots works without surprises. The audio
is cut to the PICTURE’s length, `n_frames / fps`, not `shot.duration`:
a duration that is not a whole number of frames (2.6 s at 24 fps is 62.4)
gets `round(d * fps)` frames, and an audio track padded to `d` made the
shot’s container longer than its picture — the concat then advanced by the
container and left sub-frame holes in the film’s video timestamps, which
ffprobe reads as `r_frame_rate=120/1` (an#195).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### an.media.mp4.stage_audio_inputs(shot, ctx, work_dir)

Write the shot’s per-dialogue audio bytes to disk and return (path, delay)s.

Looks up each `dialogue.audio_ref` in `mall["audio"]`. Lines without
an audio_ref or duration are skipped silently. Returns `[]` when no
audio is available, so the caller can use a video-only path.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]


# _autosummary/an.media.shutter.html.md

# an.media.shutter

The temporal half of the frame stage: average several instants into one frame.

`supersample.py` resolves a frame SPATIALLY — k x k pixels into one. This module
resolves it TEMPORALLY — the screenshots taken at each of a frame’s sample
instants (`RenderContext.frame_samples`, built by
[`an.frame_clock.FrameClock`](_autosummary/an.frame_clock.html.md#an.frame_clock.FrameClock)) into the one frame a camera with an open
shutter would have recorded. Same place, same rules: the resolve runs in the
frame stage, so everything downstream still sees PNGs at the declared size, and
the rounding is spelled out rather than inherited.

**One sample is free.** A frame with a single instant keeps the bytes the
spatial path produces (Chromium’s own, at `supersample == 1`), so a render
without an open shutter is byte-identical to one from before this module.

Engine-independent, like the spatial half: moved from
`an/adapters/cutout/shutter.py` (an#247), which re-exports it.

### Functions

| [`check_frame_samples`](_autosummary/an.media.shutter.html.md#an.media.shutter.check_frame_samples)(frame_samples, \*, ...)   | Validate `frame_samples` against the render it is for; normalise to tuples.   |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`mean_png_bytes`](_autosummary/an.media.shutter.html.md#an.media.shutter.mean_png_bytes)(shots, \*, factor)             | Spatially resolve each screenshot by `factor`, then average them in time.     |
| [`temporal_mean`](_autosummary/an.media.shutter.html.md#an.media.shutter.temporal_mean)(frames)                         | `k` equal-shape uint8 frames -> their exact per-pixel mean, uint8.            |

### Exceptions

| [`ShutterError`](_autosummary/an.media.shutter.html.md#an.media.shutter.ShutterError)   | Frame samples a render cannot honour.   |
|-----------------------------------------------------------------|-----------------------------------------|

### *exception* an.media.shutter.ShutterError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

Frame samples a render cannot honour.

### an.media.shutter.check_frame_samples(frame_samples, , total_frames, duration)

Validate `frame_samples` against the render it is for; normalise to tuples.

Checked before a browser launches, for `check_factor`’s reason: the render
costs minutes and this costs microseconds.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> check_frame_samples(None, total_frames=3, duration=0.1) is None
True
>>> check_frame_samples([[0.0], [0.5, 0.6]], total_frames=2, duration=1.0)
((0.0,), (0.5, 0.6))
>>> check_frame_samples([[0.0]], total_frames=2, duration=1.0)
Traceback (most recent call last):
  ...
an.media.shutter.ShutterError: frame_samples has 1 frame(s) but this render has 2; a frame clock must describe every frame, and only those
```

### an.media.shutter.mean_png_bytes(shots, , factor)

Spatially resolve each screenshot by `factor`, then average them in time.

One screenshot takes exactly the spatial path — the same bytes a render
without a shutter writes.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### an.media.shutter.temporal_mean(frames)

`k` equal-shape uint8 frames -> their exact per-pixel mean, uint8.

Rounded half-to-even, the rule `block_mean_resolve` spells out, so the
temporal and spatial resolves agree about every tie. The mean is of the
ENCODED (sRGB) values, as the spatial resolve’s is — not of linear light, so
a smear’s profile is not exactly a physical sensor’s. The centroid of what
is drawn is still the average of the sampled positions, which is the
property the impact ground truth relies on.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> a, b = np.full((1, 2, 3), 1, np.uint8), np.full((1, 2, 3), 2, np.uint8)
>>> temporal_mean([a, b])[0, 0].tolist()   # 1.5 -> 2 (even)
[2, 2, 2]
>>> temporal_mean([a, a + 2])[0, 0].tolist()   # 2.0 exactly
[2, 2, 2]
>>> temporal_mean([a]) is a
True
```


# _autosummary/an.media.sinks.html.md

# an.media.sinks

Frame sinks: a frame directory in, one deliverable out – one sink per format.

A sink reads the frame directory the frame stage writes ([`an.media.frames`](_autosummary/an.media.frames.html.md#module-an.media.frames):
one PNG per output frame, at the declared size) and writes one deliverable. It
knows nothing about what drew the frames, so a stage render, a Manim shot’s
frames or a `burns` crop sequence go through the same three:

| name   | what it writes                                                                                             | knobs            |
|--------|------------------------------------------------------------------------------------------------------------|------------------|
| `mp4`  | H.264, the pinned argv ([`an.media.mp4`](_autosummary/an.media.mp4.html.md#module-an.media.mp4)) | `pix_fmt`        |
| `gif`  | the one palette recipe ([`an.media.gif`](_autosummary/an.media.gif.html.md#module-an.media.gif)) | `crop`, `fps`, … |
| `png`  | the frames themselves, copied to a directory                                                               | –                |

New formats register by name ([`register_sink()`](_autosummary/an.media.sinks.html.md#an.media.sinks.register_sink)) – a WebM sink, say –
without editing this module. Browser-side sinks (WebCodecs) belong to the
TypeScript side (`previz`) and are not duplicated here (core study §2.8).

```pycon
>>> sorted(sink_names())
['gif', 'mp4', 'png']
>>> get_sink("gif", crop="10:10:0:0").crop
'10:10:0:0'
```

### Functions

| [`get_sink`](_autosummary/an.media.sinks.html.md#an.media.sinks.get_sink)(name, \*\*knobs)                   | A sink by name, built with `knobs`; the error lists what exists.   |
|----------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`register_sink`](_autosummary/an.media.sinks.html.md#an.media.sinks.register_sink)(name, factory, \*[, replace]) | Register a sink factory under `name`; refuse a silent replacement. |
| [`sink_names`](_autosummary/an.media.sinks.html.md#an.media.sinks.sink_names)()                                | The registered sink names.                                         |

### Classes

| [`FrameSink`](_autosummary/an.media.sinks.html.md#an.media.sinks.FrameSink)(\*args, \*\*kwargs)                 | Writes a frame directory as one deliverable.                                                                                                         |
|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`GifSink`](_autosummary/an.media.sinks.html.md#an.media.sinks.GifSink)([crop, fps, width, max_colours, ...]) | The demo gallery's palette recipe.                                                                                                                   |
| [`Mp4Sink`](_autosummary/an.media.sinks.html.md#an.media.sinks.Mp4Sink)([pix_fmt, name, suffix])              | The silent picture mux (the shot's audio is laid by [`an.media.mp4.mux_shot()`](_autosummary/an.media.mp4.html.md#an.media.mp4.mux_shot)). |
| [`PngSequenceSink`](_autosummary/an.media.sinks.html.md#an.media.sinks.PngSequenceSink)([name, suffix])               | The frames themselves, renumbered from 0 into `output` (a directory).                                                                                |

### *class* an.media.sinks.FrameSink(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Writes a frame directory as one deliverable.

#### name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The registry name, and the format’s usual name.

#### suffix *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The deliverable’s suffix (`".mp4"`), or `""` for a directory.

#### write(frames_dir, output, , fps)

Write `frames_dir` (rendered at `fps`) to `output`; return it.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### *class* an.media.sinks.GifSink(crop='', fps=12, width=480, max_colours=128, name='gif', suffix='.gif')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The demo gallery’s palette recipe.

### *class* an.media.sinks.Mp4Sink(pix_fmt=None, name='mp4', suffix='.mp4')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The silent picture mux (the shot’s audio is laid by [`an.media.mp4.mux_shot()`](_autosummary/an.media.mp4.html.md#an.media.mp4.mux_shot)).

`pix_fmt=None` is the module default AT CALL TIME, the seam the bench pulls.

### *class* an.media.sinks.PngSequenceSink(name='png', suffix='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The frames themselves, renumbered from 0 into `output` (a directory).

```pycon
>>> import tempfile
>>> src, out = Path(tempfile.mkdtemp()), Path(tempfile.mkdtemp()) / "seq"
>>> _ = frame_path(src, 0).write_bytes(b"png")
>>> sorted(p.name for p in PngSequenceSink().write(src, out, fps=24).iterdir())
['frame_000000.png']
```

### an.media.sinks.get_sink(name, \*\*knobs)

A sink by name, built with `knobs`; the error lists what exists.

* **Return type:**
  [`FrameSink`](_autosummary/an.media.sinks.html.md#an.media.sinks.FrameSink)

### an.media.sinks.register_sink(name, factory, , replace=False)

Register a sink factory under `name`; refuse a silent replacement.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.media.sinks.sink_names()

The registered sink names.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/an.media.supersample.html.md

# an.media.supersample

Render bigger, then resolve back exactly — the supersample knob’s two halves.

Engine-independent: any engine whose frames come back `k` times the declared
size is resolved here, in the frame stage (`an.engines.capture`). Moved from
`an/adapters/cutout/supersample.py` (an#247), which re-exports it. The first
paragraph below is about the stage engine, whose PixiJS options are where the
`k`-times backbuffer comes from; it stays here because it is the reason the
resolve exists at all.

**\`autoDensity: false\` is the whole plumbing finding, and it is load-bearing.**
`resolution: k` alone reproduces the failure it exists to avoid: with
`autoDensity: true` PixiJS sets the canvas CSS size to the *logical* size, so
Chromium composites the k-times backbuffer down before the screenshot — a blind
browser downscale, no filter choice, no record that it happened. Measured on a
declared 320x240 scene: neither key -> 320x240 PNGs; `resolution: 2,
autoDensity: false` -> 640x480; `resolution: 2, autoDensity: true` -> 320x240.
It is the option whose name most suggests it is the right one.

\*\*The resolve is an exact k x k block mean, and calling it a filter would be
wrong\*\* — at an integer ratio it *is* the supersample resolve. Measured against
the alternatives on all six corpus scenes: PIL’s `BOX` agrees with it to four
decimals, and lanczos triples the edge band on the most idiom-like scene
(+208.8% on `saturated_outline`), because its negative lobes ring on hard-edged
flat fills. An ffmpeg-side `-vf scale` is refused for a second, independent
reason: it would move `x264_argv`, refusing every encode-side metric, and retire
the cross-arch verdict’s load-bearing “ffmpeg never touches a frame” clause.

\*\*Why PIL here and not `an.bench.png`.\*\* The bench’s codec exists so a committed
golden is a function of the *pixel data alone* rather than of Chromium’s libpng
settings — a goal about files that get committed and diffed, which render-path
frames are not. And it is the wrong tool for this job by an order of magnitude:
Chromium’s screenshots are Paeth-filtered on ~87% of rows (measured: 209 of 240),
which takes its scalar unfilter path at **416 ns/px against PIL’s 31 ns/px**.
Extrapolated to a 3840x2160 supersampled frame that is \*\*3.46 s of decoding per
frame\*\* versus 256 ms — more than the render itself costs. Pillow reaches every
install through `tituli`, a core dependency; it is still imported lazily, below
the early return, so the off path needs nothing.

### Module Attributes

| [`NO_SUPERSAMPLE`](_autosummary/an.media.supersample.html.md#an.media.supersample.NO_SUPERSAMPLE)   | The factor at which every code path here is a no-op rather than merely cheap.   |
|-------------------------------------------------------------------|---------------------------------------------------------------------------------|

### Functions

| [`block_mean_resolve`](_autosummary/an.media.supersample.html.md#an.media.supersample.block_mean_resolve)(frame, factor)   | `(H*k, W*k, C)` uint8 -> `(H, W, C)` uint8, by an exact `k x k` mean.   |
|--------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`check_factor`](_autosummary/an.media.supersample.html.md#an.media.supersample.check_factor)(factor)                | Validate a supersample factor, or refuse with the reason.               |
| [`resolve_png_bytes`](_autosummary/an.media.supersample.html.md#an.media.supersample.resolve_png_bytes)(data, \*, factor) | Decode a screenshot, block-mean it down by `factor`, re-encode.         |

### Exceptions

| [`SupersampleError`](_autosummary/an.media.supersample.html.md#an.media.supersample.SupersampleError)   | A supersample factor or frame that cannot be resolved exactly.   |
|---------------------------------------------------------------------|------------------------------------------------------------------|

### an.media.supersample.NO_SUPERSAMPLE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 1*

The factor at which every code path here is a no-op rather than merely cheap.
Aliased from [`an.base.DEFAULT_SUPERSAMPLE`](_autosummary/an.base.html.md#an.base.DEFAULT_SUPERSAMPLE) rather than restated: the
default and the off-switch are the same fact, and two copies of a fact drift.

### *exception* an.media.supersample.SupersampleError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A supersample factor or frame that cannot be resolved exactly.

### an.media.supersample.block_mean_resolve(frame, factor)

`(H*k, W*k, C)` uint8 -> `(H, W, C)` uint8, by an exact `k x k` mean.

Two-step, summing rows and then columns in `uint16`, rather than the
obvious `reshape(...).astype(float64).mean(axis=(1, 3))`. The two agree
**bit for bit** — asserted exhaustively over every possible 2x2 block, and
on real frames — and the two-step form is 2.3x faster at 1080p (111.7 ms
against 262.0 ms), because the cost here is the strided reduce and the
64-bit temporary, not the arithmetic.

Rounding is spelled out rather than inherited: `np.rint` is banker’s
rounding, so a block averaging exactly `.5` goes to the EVEN neighbour.
Getting that wrong changes one code value on every half-block, which is
invisible in a picture and moves every golden.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> f = np.array([[0, 0, 1, 2], [0, 4, 1, 2]], np.uint8)[..., None].repeat(3, -1)
>>> block_mean_resolve(f, 2)[0, :, 0].tolist()
[1, 2]
>>> block_mean_resolve(f, 1) is f
True
```

### an.media.supersample.check_factor(factor)

Validate a supersample factor, or refuse with the reason.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> check_factor(1), check_factor(2)
(1, 2)
>>> check_factor(0)
Traceback (most recent call last):
  ...
an.media.supersample.SupersampleError: supersample must be >= 1, got 0
```

### an.media.supersample.resolve_png_bytes(data, , factor)

Decode a screenshot, block-mean it down by `factor`, re-encode.

Returns `data` unchanged at `factor == 1`, so the un-supersampled path
keeps Chromium’s own bytes and pays nothing at all — which is what makes
this knob free when it is off.

**The early return sits above the imports deliberately.** “Off is free”
should mean free of the *dependency* too: the default path must not need
Pillow merely to decide it has nothing to do. (Found in CI, when Pillow came
only with the `cutout` extra and the default lane did not install it.)

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)


# _autosummary/an.motion.html.md

# an.motion

Motion presets: a named vocabulary of moves, as authoring macros.

`pop_in`, `hop`, `shake`, `slide_in`, `slide_out`,
`squash_stretch` and `crawl` each EXPAND to ordinary `tween` (and
`set`) actions on transform properties, composed with
[`sequence()`](_autosummary/an.ir.compose.html.md#an.ir.compose.sequence) and [`parallel()`](_autosummary/an.ir.compose.html.md#an.ir.compose.parallel). Called from
Python, nothing downstream learns a preset exists: the flat timeline, `an
validate`, the verifiers and the renderer see the same tweens an author could
have written by hand. No runtime change, and no compiled document that does not
use a preset moves by a byte.

These are the core’s presets: each moves the node it is given (an entity
container, a text block, a plane) and needs no rig. The moves that name a
rig’s parts or swap its views — `nod`, `point`, `turn`, `walk`,
`waddle`, `speech_pulse` — are the cut-out genre’s, in `cutan.motion`
(an#322); their old names here are live aliases while anything still imports
them. This module also holds what every preset is built from: the rest pose
([`rest_pose()`](_autosummary/an.motion.html.md#an.motion.rest_pose), [`stage_poses()`](_autosummary/an.motion.html.md#an.motion.stage_poses)), the landing `set`, and
[`as_leaves()`](_autosummary/an.motion.html.md#an.motion.as_leaves).

```pycon
>>> from an.ir.compose import flatten, sequence
>>> leaves = _tweens(sequence(pop_in("charlie"), hop("charlie"), shake("charlie")))
>>> [(f.action.target, f.action.property) for f in leaves][:3]
[('charlie', 'scale_x'), ('charlie', 'scale_y'), ('charlie', 'y')]
>>> round(leaves[-1].end, 3)
1.35
```

**Targets.** A preset targets the node it is given — usually the entity
container (`"charlie"`), the node a descriptor’s `bone:root` track animates
too. A target the built scene does not carry makes the render raise (the
runtime refuses an unknown node, naming the known ones); [`rest_pose()`](_autosummary/an.motion.html.md#an.motion.rest_pose)
raises for it up front, before any browser starts.

**Landing.** Every preset ends each property it moves with a `set` at the
value it ends on, so the move lands exactly whatever the frame rate or
`step_hz` (a tween ending between two frames otherwise leaves the property
where the last sampled frame had it). The `set` holds until the next tween
on that property.

**Rest.** A tween’s value is ABSOLUTE, and every preset writes its `from`
explicitly so presets chain without a jump. Each preset therefore needs the
target node’s rest value for the properties it moves; `rest=None` means the
identity pose (`x = y = rotation = 0`, `scale = 1`), which is right for
every rotation, and for `y`/`scale` of any entity without a `stage`
placement — but an entity’s `x` is laid out across the shot (`-110` and
`110` for two characters), so a move on `x` (`shake`, `slide_in`,
`slide_out`) in a shot with more than one character
wants `rest=rest_pose(shot, "charlie")`, which reads the value off the
compiler’s own scene builder rather than restating its layout.

**scene.md: play a preset by name** (an#166; `play` is the cut-out genre’s
action kind, and `cutan.motion.PRESETS` lists these presets with its own). `{kind: play, target:
charlie, animation: hop, args: {height: 30}, start: 1.0}` expands to exactly
this module’s tweens at compile, with `rest` the pose the node HAS at the
play’s start — the built scene’s (stage placement, layout) overridden by the
sets and tweens before it (an#212) — so no `rest=`, and a preset after a move
starts where the move left it (an entrance in [`HOME_PRESETS`](_autosummary/an.motion.html.md#an.motion.HOME_PRESETS) lands on the
built pose instead); `args` are the preset’s keyword arguments. A character
descriptor animation of the same name WINS; `an validate` and the compiler
decide both through `cutan.characters.play.play_problems()`. `duration`
stretches the move and `speed` divides it; `loop` is refused. In a
`sequence` a `play` without a `duration` occupies the preset’s own
length divided by `speed`, so two in a row run one after the other.
[`as_leaves()`](_autosummary/an.motion.html.md#an.motion.as_leaves) remains for a preset composed in Python and written into
`scene.md` as plain, hand-editable tweens (a composition tree round-trips
too since an#241, but verbatim, as its JSON form).

### Module Attributes

| [`OVERSHOOT`](_autosummary/an.motion.html.md#an.motion.OVERSHOOT)       | A cubic-Bézier that overshoots its target by about 10% and settles back (CSS "easeOutBack").                                                                                                                                                                                                    |
|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`PRESETS`](_autosummary/an.motion.html.md#an.motion.PRESETS)         | the moves that need no rig.                                                                                                                                                                                                                                                                     |
| [`PRESET_VERSIONS`](_autosummary/an.motion.html.md#an.motion.PRESET_VERSIONS) | bump a preset's version in the SAME change that makes it expand differently for the same args, so every shot that plays it re-renders visibly instead of silently ([`an.semantic`](_autosummary/an.semantic.html.md#module-an.semantic) folds it into the shot's vocabulary digest). |
| [`HOME_PRESETS`](_autosummary/an.motion.html.md#an.motion.HOME_PRESETS)    | Presets whose `rest` is the node's HOME — where an entrance LANDS — rather than where the node is when the move starts.                                                                                                                                                                         |

### Functions

| [`as_leaves`](_autosummary/an.motion.html.md#an.motion.as_leaves)(action, \*[, start])                   | `action` as top-level leaves that `scene.md` can round-trip.                                                                                                                                                                                                                         |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`crawl`](_autosummary/an.motion.html.md#an.motion.crawl)(target, \*[, distance, duration, ...])     | An opening crawl: lay `target` on a plane tilted away, and slide it up and away.                                                                                                                                                                                                     |
| [`hop`](_autosummary/an.motion.html.md#an.motion.hop)(target, \*[, height, duration, rest])        | Jump up by `height` scene pixels and land back where it started.                                                                                                                                                                                                                     |
| [`pop_in`](_autosummary/an.motion.html.md#an.motion.pop_in)(target, \*[, duration, easing, rest])     | Grow from nothing to full size, overshooting and settling (an entrance).                                                                                                                                                                                                             |
| [`rest_pose`](_autosummary/an.motion.html.md#an.motion.rest_pose)(shot, target, \*[, mall])              | The rest values of `target`'s node as the compiler builds `shot`.                                                                                                                                                                                                                    |
| [`shake`](_autosummary/an.motion.html.md#an.motion.shake)(target, \*[, amplitude, duration, ...])    | Tremble side to side `cycles` times and come back to rest (on `x`).                                                                                                                                                                                                                  |
| [`stage_poses`](_autosummary/an.motion.html.md#an.motion.stage_poses)(shot, \*[, mall, width, height])     | `{node path: rest pose}` for every node the compiler builds for `shot`'s stage — what [`rest_pose()`](_autosummary/an.motion.html.md#an.motion.rest_pose) reads one entry of, and what `an validate` checks a preset `play`'s node and every `set`/`tween` target against (an#166, an#193). |
| [`slide_in`](_autosummary/an.motion.html.md#an.motion.slide_in)(target, \*[, from_side, distance, ...]) | Whip in from `distance` pixels off to one side, overshoot, and settle.                                                                                                                                                                                                               |
| [`slide_out`](_autosummary/an.motion.html.md#an.motion.slide_out)(target, \*[, to_side, distance, ...])  | Exit `distance` pixels off to one side, accelerating (an exit).                                                                                                                                                                                                                      |
| [`squash_stretch`](_autosummary/an.motion.html.md#an.motion.squash_stretch)(target, \*[, amount, ...])        | Squash (wide and short), stretch (narrow and tall), then settle.                                                                                                                                                                                                                     |

### an.motion.HOME_PRESETS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'pop_in', 'slide_in'})*

Presets whose `rest` is the node’s HOME — where an entrance LANDS — rather
than where the node is when the move starts. Played by name these read the
BUILT pose (`slide_out` then `slide_in` comes back home; `pop_in` after a
`set` of the scales to 0 grows to full size); every other preset moves
relative to where the node IS at its start (an#212).

### an.motion.OVERSHOOT *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= (0.34, 1.56, 0.64, 1.0)*

A cubic-Bézier that overshoots its target by about 10% and settles back
(CSS “easeOutBack”). The compiler and both evaluators take any 4-point
Bézier on a numeric channel, and nothing clamps `y` to `[0, 1]`.

### an.motion.PRESETS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[...], [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[SetAction](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), Tag(tag=[set](https://docs.python.org/3/builtins/stdtypes.html#set))] | [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[TweenAction](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), Tag(tag=tween)] | [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[SequenceAction](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), Tag(tag=sequence)] | [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[ParallelAction](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), Tag(tag=parallel)] | [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[DelayAction](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), Tag(tag=delay)] | [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[LoopAction](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), Tag(tag=loop)] | [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[ExtensionAction](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction), SerializeAsAny(), Tag(tag=extension)], Discriminator(discriminator=\_action_tag, custom_error_type=[None](https://docs.python.org/3/builtins/constants.html#None), custom_error_message=[None](https://docs.python.org/3/builtins/constants.html#None), custom_error_context=[None](https://docs.python.org/3/builtins/constants.html#None))]]]* *= {'crawl': <function crawl>, 'hop': <function hop>, 'pop_in': <function pop_in>, 'shake': <function shake>, 'slide_in': <function slide_in>, 'slide_out': <function slide_out>, 'squash_stretch': <function squash_stretch>}*

the moves that need no rig. A genre’s presets
(the cut-out ones: `cutan.motion.PRESETS`, an#322) are listed beside these
by the genre, which owns the `play` that names them.

* **Type:**
  The core’s presets by name

### an.motion.PRESET_VERSIONS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'crawl': '1', 'hop': '1', 'pop_in': '1', 'shake': '1', 'slide_in': '1', 'slide_out': '1', 'squash_stretch': '1'}*

bump a preset’s
version in the SAME change that makes it expand differently for the same
args, so every shot that plays it re-renders visibly instead of silently
([`an.semantic`](_autosummary/an.semantic.html.md#module-an.semantic) folds it into the shot’s vocabulary digest). Versioning
starts here (an#248).

* **Type:**
  Each preset’s vocabulary version (ADR 0003 decision 2)

### an.motion.as_leaves(action, , start=0.0)

`action` as top-level leaves that `scene.md` can round-trip.

The markdown writer spells a leaf and the `sequence(delay(start), leaf)`
wrapper the parser produces for a `start:` key in their short form, and
writes any other composition tree verbatim (its JSON form, an#241). This
flattens a preset (or any tree) into the short form, with the same
absolute times, which is what a person editing `scene.md` wants.

A `set` keeps its absolute time in `at` instead of a wrapper.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[`Union`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)]]

```pycon
>>> leaves = as_leaves(hop("charlie"), start=1.0)
>>> [type(a).__name__ for a in leaves]
['SequenceAction', 'SequenceAction', 'SetAction']
>>> [round(f.start, 3) for a in leaves for f in flatten(a)]  # each from 0
[1.0, 1.25, 1.5]
```

### an.motion.crawl(target, , distance=2400.0, duration=30.0, start=None, tilt=0.96, perspective=1.0, fade=(700.0, 1500.0), y=None, easing='linear', rest=None)

An opening crawl: lay `target` on a plane tilted away, and slide it up and away.

Sets the plane (`rotation_x` = `tilt`, `perspective`, the far
`fade`, and the hinge’s `y` when given) at the start, then ONE tween:
`pivot_y` from `start` (default: where the pivot rests) to `start +
distance`. On a tilted node the pivot is the point of the plane on the
hinge (an#314), so the content travels `distance` scene px along the
plane; the slowing and shrinking as it recedes are the projection’s, not
the tween’s. A block centred on its origin starts with its middle on the
hinge: a negative `start` (half the block’s height and more) has it
enter from below. `fade=None` draws the plane to the horizon.

* **Return type:**
  `Union`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)]

```pycon
>>> leaves = flatten(crawl("crawl", distance=1000, duration=20, start=-300, fade=None))
>>> sorted((f.action.property, getattr(f.action, "value", None)) for f in leaves
...        if isinstance(f.action, SetAction) and f.start == 0)
[('perspective', 1.0), ('rotation_x', 0.96)]
>>> [(f.action.from_value, f.action.to_value, f.end) for f in _tweens(crawl("crawl",
...     distance=1000, duration=20, start=-300))]
[(-300.0, 700.0, 20.0)]
```

### an.motion.hop(target, , height=40.0, duration=0.5, rest=None)

Jump up by `height` scene pixels and land back where it started.

* **Return type:**
  `Union`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)]

```pycon
>>> [(f.action.from_value, f.action.to_value) for f in _tweens(hop("charlie", height=30))]
[(0.0, -30.0), (-30.0, 0.0)]
```

### an.motion.pop_in(target, , duration=0.45, easing=(0.34, 1.56, 0.64, 1.0), rest=None)

Grow from nothing to full size, overshooting and settling (an entrance).

Scales the target from 0 to its rest scale. Before the preset starts the
target shows at its rest pose: to keep it hidden until it pops, start the
preset at the target’s first frame (or hold `scale_x`/`scale_y` at 0
with a `set` before it).

* **Return type:**
  `Union`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)]

```pycon
>>> [(f.action.property, f.action.from_value, f.action.to_value)
...  for f in _tweens(pop_in("charlie"))]
[('scale_x', 0.0, 1.0), ('scale_y', 0.0, 1.0)]
```

### an.motion.rest_pose(shot, target, , mall=None)

The rest values of `target`’s node as the compiler builds `shot`.

Compiles the shot’s STAGE — its entities, without actions, dialogue or
camera — through the cutout compiler’s own scene builder, so the layout
(`-110`/`110` for two characters), a `stage` placement and a stage
scale are read, never restated. Pass the same `mall` you render with:
a descriptor rig is built from its character store.

(These examples build character entities, the cut-out genre’s; they are not run here.)

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> from an.ir.schema import AssetRef
>>> two = Shot(id="s", entities=[
...     AssetRef(kind="character", id=n, store="characters", ref=n) for n in ("a", "b")])
>>> rest_pose(two, "a")["x"], rest_pose(two, "b")["x"]
(-110.0, 110.0)
>>> rest_pose(two, "a/head")["y"]
-55.0
```

### an.motion.shake(target, , amplitude=8.0, duration=0.4, cycles=3, rest=None)

Tremble side to side `cycles` times and come back to rest (on `x`).

* **Return type:**
  `Union`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)]

```pycon
>>> [f.action.to_value for f in _tweens(shake("charlie", amplitude=5, cycles=2))]
[5.0, -5.0, 5.0, -5.0, 0.0]
>>> [f.action.to_value for f in _tweens(shake("charlie", cycles=1, rest={"x": -110}))]
[-102.0, -118.0, -110.0]
```

### an.motion.slide_in(target, , from_side='left', distance=600.0, duration=0.35, easing=(0.34, 1.56, 0.64, 1.0), rest=None)

Whip in from `distance` pixels off to one side, overshoot, and settle.

* **Return type:**
  `Union`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)]

```pycon
>>> [(f.action.from_value, f.action.to_value) for f in _tweens(slide_in("charlie", distance=400))]
[(-400.0, 0.0)]
```

### an.motion.slide_out(target, , to_side='right', distance=600.0, duration=0.35, easing='ease_in', rest=None)

Exit `distance` pixels off to one side, accelerating (an exit).

* **Return type:**
  `Union`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)]

```pycon
>>> [(f.action.from_value, f.action.to_value) for f in _tweens(slide_out("charlie", to_side="left"))]
[(0.0, -600.0)]
```

### an.motion.squash_stretch(target, , amount=0.2, duration=0.36, rest=None)

Squash (wide and short), stretch (narrow and tall), then settle.

Scales about the target’s own origin (for the procedural rig, the torso’s
centre). Volume is roughly kept: one axis grows by what the other loses.

* **Return type:**
  `Union`[[`SetAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SetAction), [`TweenAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction), [`SequenceAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.SequenceAction), [`ParallelAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ParallelAction), [`DelayAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.DelayAction), [`LoopAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.LoopAction), [`ExtensionAction`](_autosummary/an.ir.schema.html.md#an.ir.schema.ExtensionAction)]

```pycon
>>> [[round(f.action.to_value, 2) for f in _tweens(squash_stretch("c"))
...   if f.action.property == p] for p in ("scale_x", "scale_y")]
[[1.2, 0.9, 1.0], [0.8, 1.1, 1.0]]
```

### an.motion.stage_poses(shot, , mall=None, width=None, height=None)

`{node path: rest pose}` for every node the compiler builds for
`shot`’s stage — what [`rest_pose()`](_autosummary/an.motion.html.md#an.motion.rest_pose) reads one entry of, and what
`an validate` checks a preset `play`’s node and every `set`/`tween`
target against (an#166, an#193). `width`/`height` (default: the
compiler’s) matter to text, whose line breaks depend on the frame.

(These examples build character entities, the cut-out genre’s; they are not run here.)

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

```pycon
>>> from an.ir.schema import AssetRef
>>> one = Shot(id="s", entities=[AssetRef(kind="character", id="c", store="characters", ref="c")])
>>> poses = stage_poses(one)
>>> "c/right_arm" in poses, poses["c/head"]["y"]
(True, -55.0)
```


# _autosummary/an.orchestrate.html.md

# an.orchestrate

Orchestrator: validate → audio → render → verify.

Phase 5 ships `orchestrate(project_dir, ...)` — the high-level flow that
ties together validation, audio synthesis, rendering, and verification.
The full iterative edit loop (free-text “make Maya’s laugh longer” →
re-render only the affected shot) lives in the `an` skill, which calls
into these primitives.

```pycon
>>> from an.orchestrate import OrchestratorReport
>>> r = OrchestratorReport()
>>> r.success
True
```

### Functions

| [`iterate`](_autosummary/an.orchestrate.html.md#an.orchestrate.iterate)(project_dir, instruction, \*\*kwargs)    | Apply a free-text edit instruction.                                                                                                                                                                                     |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`orchestrate`](_autosummary/an.orchestrate.html.md#an.orchestrate.orchestrate)(project_dir, \*[, output_name, ...]) | Run the full pipeline.                                                                                                                                                                                                  |
| [`render_project`](_autosummary/an.orchestrate.html.md#an.orchestrate.render_project)(project_dir, \*\*kwargs)          | Render the project's scene to a single mp4 under `output/` — the orchestrator's name for [`an.render.render_project()`](_autosummary/an.render.html.md#an.render.render_project), every keyword forwarded. |
| [`validate_project`](_autosummary/an.orchestrate.html.md#an.orchestrate.validate_project)(project_dir, \*[, fps, ...])    | Schema + semantic validation of the scene at `project_dir`.                                                                                                                                                             |

### Classes

| [`OrchestratorReport`](_autosummary/an.orchestrate.html.md#an.orchestrate.OrchestratorReport)([success, output_path, ...])   | Outcome of an end-to-end orchestrated run.   |
|----------------------------------------------------------------------------------------------------|----------------------------------------------|

### *class* an.orchestrate.OrchestratorReport(success=True, output_path=None, validation=None, verifications=<factory>, error=None, root=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of an end-to-end orchestrated run.

#### merge_verification(vr)

Add `vr` — less any finding this report already holds: the render
report repeats what the pre-render validation found (a synthesized
line still past its shot), and one finding is reported once (an#309).
The SAME finding: severity, path, location and description (its paths
made portable) — so a render that escalates a warning to an error, or
locates it elsewhere, is reported. `vr`’s verdict is kept.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### root *: [Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [None](https://docs.python.org/3/builtins/constants.html#None)*

descriptions are compared with their
paths made portable (the render report stores them so; an#309).

* **Type:**
  The project root, when known

### an.orchestrate.iterate(project_dir, instruction, \*\*kwargs)

Apply a free-text edit instruction. Returns an IterateResult.

Thin re-export for consistency with the rest of the orchestrator surface;
the real implementation lives in `an.iterate`.

### an.orchestrate.orchestrate(project_dir, , output_name='main', verifiers=None, skip_render=False, tts=None, lipsync='offline', parallel=None, language='en')

Run the full pipeline. Returns a structured outcome.

Phases:

> 1. Validate (schema + semantic). Hard fail if the schema is broken.
> 2. Pre-render verifiers (any that accept `render=None`).
> 3. Render (audio is auto-run inside `render` when needed).
> 4. Post-render verifiers.

`verifiers` defaults to `[LayoutLintVerifier(), MediaQualityVerifier()]`
— the second one is why `an.verify.media.ssim`’s threshold is load-bearing
and must not be retuned casually. Pass an empty list
to skip verification, or include `HumanInTheLoopVerifier()` to prompt.
`skip_render=True` runs validation + lint only.

`tts` and `lipsync` accept either a provider name string or a
provider instance — useful for callers (e.g. `muvid`) that want
to inject a `an.audio.WordTimingsLipSync` driven by their
own alignment store, instead of letting `an` re-transcribe. `tts`
defaults to each voice’s own provider (an#305), as `an render` does.

* **Return type:**
  [`OrchestratorReport`](_autosummary/an.orchestrate.html.md#an.orchestrate.OrchestratorReport)

### an.orchestrate.render_project(project_dir, \*\*kwargs)

Render the project’s scene to a single mp4 under `output/` — the
orchestrator’s name for [`an.render.render_project()`](_autosummary/an.render.html.md#an.render.render_project), every keyword
forwarded.

It used to re-declare the leaf’s parameters, and the two drifted: the CLI
(`an render`) passed `supersample`, `pix_fmt`, `step_hz` and `language`
here from the day each flag landed, and this wrapper refused all four with
a `TypeError` — invisible because the CLI test stubbed THIS function
rather than the leaf (an#98 review). A pass-through cannot drift.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.orchestrate.validate_project(project_dir, , fps=None, strict_assets=False)

Schema + semantic validation of the scene at `project_dir`.

A `scene.md` that does not PARSE — a dialogue line in no accepted shape
(an#96), a malformed YAML block — is a Finding, not a traceback: `an
validate` exists to print findings, and it used to be the one tool that
stack-dumped on the error it should report.

`fps` is the frame rate the render will use when it is not the scene’s
(`an render --fps`): the checks that depend on it (`step_hz`, a line
heard during a dissolve) use it, as the render will (an#435).

What loading the scene WARNED about (a retired camera field dropped on
read, a migration’s notice) is a warning finding too (an#454): a Python
warning is invisible to an agent reading `an validate`’s findings.

`strict_assets` (an#456) judges the scene as `an render
--strict-assets` will: each stage shot is compiled the way the render
compiles it, refusing stand-ins, and what it refuses is an error on that
shot; the library pins are checked strictly too.

* **Return type:**
  [`ValidationReport`](_autosummary/an.ir.validate.html.md#an.ir.validate.ValidationReport)


# _autosummary/an.paint.html.md

# an.paint

Paint: what fills a shape when one flat colour is not enough – a gradient.

A **gradient** blends colour **stops** (CSS’s *color stops*: a colour at an
offset from 0 to 1 along the gradient) either along a line (**linear**) or
outward from a centre (**radial**). The vocabulary is CSS’s
(`linear-gradient` / `radial-gradient`), so an author who knows one knows
the other:

- `angle` (linear) is the CSS gradient angle in degrees: `0` runs to the
  top, `90` to the right, `180` (the default) to the bottom. As in CSS the
  gradient line is as long as it must be for the box’s corners to sit on the
  first and last stop.
- `center` and `radius` (radial) are fractions of the box. In a box that is
  not square the gradient is an ellipse with the box’s proportions (CSS’s
  `ellipse`); `radius: 0.5` touches the sides of a centred box.
- Colours between stops are interpolated in sRGB, as CSS and SVG do by default;
  beyond the first and last stop the end colours hold.

Colours are hex strings (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`), the
representation the StylePack uses everywhere. Stops may be written as plain
colours, which are spread evenly:

```pycon
>>> g = Gradient(stops=["#101030", "#f0d090"])
>>> [(s.offset, s.color) for s in g.stops]
[(0.0, '#101030'), (1.0, '#f0d090')]
>>> Gradient(type="radial", stops=["#fff", "#0000"], radius=0.75).radius
0.75
```

A field that would do nothing is refused rather than ignored:

```pycon
>>> Gradient(type="radial", stops=["#fff", "#000"], angle=90)
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: ...
```

This is genre-neutral core vocabulary: a plane of the 2D stage draws one
([`an.stage.environments.PlaneArt`](_autosummary/an.stage.environments.html.md#an.stage.environments.PlaneArt)), and a StylePack names them as
**gradient roles** ([`an.styles.StylePack.gradients`](_autosummary/an.styles.html.md#an.styles.StylePack.gradients)).

### Module Attributes

| [`DFLT_GRADIENT_ANGLE`](_autosummary/an.paint.html.md#an.paint.DFLT_GRADIENT_ANGLE)   | top to bottom.   |
|------------------------------------------------------------------------|------------------|

### Functions

| [`rgba_of`](_autosummary/an.paint.html.md#an.paint.rgba_of)(color)   | A hex colour as `(r, g, b, a)` in `0..1`.   |
|-------------------------------------------------------------------|---------------------------------------------|

### Classes

| [`Gradient`](_autosummary/an.paint.html.md#an.paint.Gradient)(\*\*data)     | A linear or radial gradient between colour stops (see the module docstring).   |
|-------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`GradientStop`](_autosummary/an.paint.html.md#an.paint.GradientStop)(\*\*data) | A colour at an offset along the gradient (`0` start, `1` end).                 |

### an.paint.DFLT_GRADIENT_ANGLE *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 180.0*

top to bottom.

* **Type:**
  CSS’s default gradient direction

### *class* an.paint.Gradient(\*\*data)

Bases: `BaseModel`

A linear or radial gradient between colour stops (see the module docstring).

#### angle *: [float](https://docs.python.org/3/builtins/functions.html#float)*

the CSS gradient angle, degrees (0 up, 90 right, 180 down).

* **Type:**
  Linear only

#### center *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]*

the centre, in fractions of the box.

* **Type:**
  Radial only

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### radius *: [float](https://docs.python.org/3/builtins/functions.html#float)*

the radius, in fractions of the box (an ellipse in a
non-square box).

* **Type:**
  Radial only

### *class* an.paint.GradientStop(\*\*data)

Bases: `BaseModel`

A colour at an offset along the gradient (`0` start, `1` end).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.paint.rgba_of(color)

A hex colour as `(r, g, b, a)` in `0..1`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> rgba_of("#ff000080")
(1.0, 0.0, 0.0, 0.5019607843137255)
```


# _autosummary/an.paths.html.md

# an.paths

Moved to [`an.stage.paths`](_autosummary/an.stage.paths.html.md#module-an.stage.paths) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.preview.html.md

# an.preview

Moved to [`an.stage.preview`](_autosummary/an.stage.preview.html.md#module-an.stage.preview) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.probe.html.md

# an.probe

`an probe`: a shot’s frame at chosen instants, through the very path `render` draws it (an#347).

Looking at one moment of a shot used to mean a whole render, or a throwaway
script that staged the scene its own way and drew something else. A probe
prepares the shot exactly as [`an.render.render()`](_autosummary/an.render.html.md#an.render.render) does (`_prepare_shots`:
settled length, burned captions, the style pack, the resolved knobs), then asks
the shot’s own renderer for the frames showing at the instants given:

- a frame-stage renderer (the stage engine, the cut-out renderer) opens the
  session `render` opens and captures through
  [`an.engines.capture.capture_frames()`](_autosummary/an.engines.capture.html.md#an.engines.capture.capture_frames) — supersample, frame clock and
  > canvas readback included — only the frames asked for
  > ([`an.engines.frame_stage.FrameStageRenderer.probe_frames()`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.FrameStageRenderer.probe_frames));
- a renderer that owns its clock (Manim) serves the frames from the picture a
  render stored, or refuses with the remedy;
- any other renderer refuses: it has no `probe_frames`.

Speech plays as the last render (or synthesis) stamped it: a probe never
synthesises a line. Nothing is cached, written to the shot cache or archived.

**Rights.** A frame showing material that is not publishable (`an credits`
for the shot says private or unknown) is refused at a path inside a git work
tree that does not ignore it; the default folder, `artifacts/probes/`, is
ignored by `an init` and added to an older project’s `.gitignore`.

### Functions

| [`frame`](_autosummary/an.probe.html.md#an.probe.frame)(project, shot, t, \*\*kwargs)             | PNG bytes of shot `shot`'s film frame at `t` seconds (see [`frames()`](_autosummary/an.probe.html.md#an.probe.frames)).   |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| [`frames`](_autosummary/an.probe.html.md#an.probe.frames)(project, shot, times, \*[, supersample]) | PNG bytes of shot `shot`'s film frame at each of `times` (seconds into the shot).                                       |
| [`probe`](_autosummary/an.probe.html.md#an.probe.probe)(project_dir, shot, at, \*[, out, ...])    | Write shot `shot`'s frames at `at` to one PNG (a grid for several instants).                                            |

### Exceptions

| [`ProbeError`](_autosummary/an.probe.html.md#an.probe.ProbeError)   | A probe cannot draw what was asked: the message says why and what to do.   |
|---------------------------------------------------------------|----------------------------------------------------------------------------|

### *exception* an.probe.ProbeError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

A probe cannot draw what was asked: the message says why and what to do.

### an.probe.frame(project, shot, t, \*\*kwargs)

PNG bytes of shot `shot`’s film frame at `t` seconds (see [`frames()`](_autosummary/an.probe.html.md#an.probe.frames)).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### an.probe.frames(project, shot, times, , supersample=1)

PNG bytes of shot `shot`’s film frame at each of `times` (seconds into the shot).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

project: a loaded [`Project`](_autosummary/an.project.html.md#an.project.Project) or its folder
supersample: as `an render --supersample` (the frame is the film’s only

> if it matches the render’s)

### an.probe.probe(project_dir, shot, at, , out=None, columns=None, supersample=1, allow_private_here=False)

Write shot `shot`’s frames at `at` to one PNG (a grid for several instants).

out: the PNG to write (default `<project>/artifacts/probes/<shot>@<t>.png`)
columns: cells per row of a grid (default: a square grid)
allow_private_here: write not-publishable frames inside a git work tree

> that does not ignore the path
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.project.html.md

# an.project

Project init/load/save — the on-disk anatomy of an an project.

Layout (from spec §11):

> my-scene/
> ├── an.toml
> ├── scene.md
> ├── ir/scene.json
> ├── assets/{characters,props,environments,voices,styles,sounds}/
> ├── artifacts/{audio,visemes,shots,previews}/
> ├── output/
> └── .an/{decisions.jsonl,verifier_runs/,memory.md}

### Module Attributes

| [`RENDER_REPORTS_GITIGNORE`](_autosummary/an.project.html.md#an.project.RENDER_REPORTS_GITIGNORE)   | A render report records what a render on THIS machine found — warnings, exception text — so it is per-machine output, not project source (an#254).   |
|-----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`PROBES_GITIGNORE`](_autosummary/an.project.html.md#an.project.PROBES_GITIGNORE)           | Probe frames and contact sheets (`an probe`, `an library sheet`, an#347): looks at work in progress, which may show private-study material.          |
| [`PROJECT_GITIGNORE`](_autosummary/an.project.html.md#an.project.PROJECT_GITIGNORE)          | What a project's `.gitignore` keeps out of version control (`an init` adds each line a `.gitignore` lacks, never removing one).                      |

### Functions

| [`init`](_autosummary/an.project.html.md#an.project.init)(project_dir, \*[, name, force])   | Create a fresh an project at `project_dir`.                                                                                                                                                                                                                                                                                                                                                           |
|-----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`keep_out_of_git`](_autosummary/an.project.html.md#an.project.keep_out_of_git)(pdir, lines)           | Add `lines` to an OLDER project's `.gitignore` (an `an init` from before they existed left them out) — conservatively: never a line whose folder the file already says anything about (a project may have chosen to commit its reports, `!artifacts/render_reports/`), never through a symlink (a shared ignore file), and never creating a `.gitignore` in a project that is not in a git work tree. |
| [`keep_reports_out_of_git`](_autosummary/an.project.html.md#an.project.keep_reports_out_of_git)(pdir)          | Add [`RENDER_REPORTS_GITIGNORE`](_autosummary/an.project.html.md#an.project.RENDER_REPORTS_GITIGNORE) to an older project's `.gitignore` the first time a render writes a report (an#309), by [`keep_out_of_git()`](_autosummary/an.project.html.md#an.project.keep_out_of_git)'s rules.                                                                                                                                                     |
| [`load`](_autosummary/an.project.html.md#an.project.load)(project_dir, \*[, check_kinds])   | Load an existing project.                                                                                                                                                                                                                                                                                                                                                                             |
| [`save`](_autosummary/an.project.html.md#an.project.save)(project)                          | Persist a Project's current scene back to disk (md + json).                                                                                                                                                                                                                                                                                                                                           |

### Classes

| [`Project`](_autosummary/an.project.html.md#an.project.Project)(root, mall, scene)   | A loaded an project: directory + mall + current scene.   |
|-------------------------------------------------------------------------------|----------------------------------------------------------|

### an.project.PROBES_GITIGNORE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('artifacts/probes/',)*

Probe frames and contact sheets (`an probe`, `an library sheet`,
an#347): looks at work in progress, which may show private-study material.

### an.project.PROJECT_GITIGNORE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('artifacts/render_reports/', 'artifacts/probes/')*

What a project’s `.gitignore` keeps out of version control (`an init`
adds each line a `.gitignore` lacks, never removing one).

### *class* an.project.Project(root, mall, scene)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A loaded an project: directory + mall + current scene.

### an.project.RENDER_REPORTS_GITIGNORE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('artifacts/render_reports/',)*

A render report records what a render on THIS machine found — warnings,
exception text — so it is per-machine output, not project source (an#254).

### an.project.init(project_dir, , name=None, force=False)

Create a fresh an project at `project_dir`.

Idempotent unless the directory already contains a non-empty `scene.md`;
pass `force=True` to overwrite. Returns the absolute project root. The
project’s `.gitignore` gains [`PROJECT_GITIGNORE`](_autosummary/an.project.html.md#an.project.PROJECT_GITIGNORE) (lines it lacks).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.project.keep_out_of_git(pdir, lines)

Add `lines` to an OLDER project’s `.gitignore` (an `an init` from
before they existed left them out) — conservatively: never a line whose
folder the file already says anything about (a project may have chosen to
commit its reports, `!artifacts/render_reports/`), never through a
symlink (a shared ignore file), and never creating a `.gitignore` in a
project that is not in a git work tree.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.project.keep_reports_out_of_git(pdir)

Add [`RENDER_REPORTS_GITIGNORE`](_autosummary/an.project.html.md#an.project.RENDER_REPORTS_GITIGNORE) to an older project’s `.gitignore`
the first time a render writes a report (an#309), by [`keep_out_of_git()`](_autosummary/an.project.html.md#an.project.keep_out_of_git)’s rules.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.project.load(project_dir, , check_kinds=True)

Load an existing project. Reconciles scene.md / ir/scene.json first.

Registers the installed genres first ([`an.genres.load()`](_autosummary/an.genres.html.md#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’s
`play`, `expression` and `character` — 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()`](_autosummary/an.ir.validate.html.md#an.ir.validate.require_registered_kinds)): the
schema holds those as `str` (ADR 0001 decision 2), so without this a
typo’d `kind: enviroment` would load and render silently without its
backdrop. `check_kinds=False` is for `an validate`, which reports them
as findings instead.

* **Return type:**
  [`Project`](_autosummary/an.project.html.md#an.project.Project)

### an.project.save(project)

Persist a Project’s current scene back to disk (md + json).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.props.html.md

# an.props

Moved to [`an.stage.props`](_autosummary/an.stage.props.html.md#module-an.stage.props) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.props_cli.html.md

# an.props_cli

`an props …` — a prop’s asset folder from the shell (an#340).

A CORE module (`an.tools` imports it at module level), so it reaches the
stage only inside its functions: the stage is behind the core firewall.

Wired into the top-level dispatcher as the `props` namespace
(`an.tools._dispatch_namespaces`), programmatically, per pillar 8: plain
functions taking strings and returning the text to print; the business logic is
[`an.stage.prop_validate`](_autosummary/an.stage.prop_validate.html.md#module-an.stage.prop_validate). The prop’s twin of `an character validate` /
`an character contract`.

### Functions

| [`contract`](_autosummary/an.props_cli.html.md#an.props_cli.contract)()                | Print what a prop folder must hold, generated from the schema and the rig rules.   |
|----------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| [`validate`](_autosummary/an.props_cli.html.md#an.props_cli.validate)(name[, out_dir]) | Check a prop's folder (prop.json + parts/) against the rig contract, offline.      |

### an.props_cli.contract()

Print what a prop folder must hold, generated from the schema and the rig rules.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.props_cli.validate(name, out_dir='')

Check a prop’s folder (prop.json + parts/) against the rig contract, offline.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

name: the prop’s key (its folder name under out_dir), or a path to its
: folder or to its prop.json

out_dir: parent directory of the keys; defaults to ./assets/props


# _autosummary/an.raster.html.md

# an.raster

Moved to [`an.stage.raster`](_autosummary/an.stage.raster.html.md#module-an.stage.raster) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.render.html.md

# an.render

Project-level rendering: per-shot mp4 → final composited mp4 via ffmpeg concat.

The orchestrator picks a renderer per shot from the registry (matched on
`shot.renderer`) and renders each shot in isolation, then concatenates the
per-shot outputs into one final mp4 written to `project.mall["output"]`.

Phase 2D ships the cutout path; later phases register Manim / Remotion / etc.
adapters and the same flow handles them.

### Module Attributes

| [`RENDER_RUNS_DIR`](_autosummary/an.render.html.md#an.render.RENDER_RUNS_DIR)        | one directory per CACHED render run.                                                                                                                                                                                       |
|-------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`RUN_LIVE_MARKER`](_autosummary/an.render.html.md#an.render.RUN_LIVE_MARKER)        | the pid of the process rendering it (written at start).                                                                                                                                                                    |
| [`RUN_DONE_MARKER`](_autosummary/an.render.html.md#an.render.RUN_DONE_MARKER)        | written when the run delivered its film.                                                                                                                                                                                   |
| [`TMP_TOKEN`](_autosummary/an.render.html.md#an.render.TMP_TOKEN)              | What a temp-folder path becomes in a portable text ([`portable_text()`](_autosummary/an.render.html.md#an.render.portable_text)).                                                                                                     |
| [`POSIX_TEMP_DIRS`](_autosummary/an.render.html.md#an.render.POSIX_TEMP_DIRS)        | The temp folders every POSIX machine has, besides the one Python reports.                                                                                                                                                  |
| [`FINDING_GROUPS`](_autosummary/an.render.html.md#an.render.FINDING_GROUPS)         | How `an render`'s summary heads each `kind` of finding, in this order; a kind not listed (another warning category) is headed by its own name, after.                                                                      |
| [`SUMMARY_MAX_PER_GROUP`](_autosummary/an.render.html.md#an.render.SUMMARY_MAX_PER_GROUP)  | At most this many findings of one kind are listed in the summary.                                                                                                                                                          |
| [`UNKNOWN_LIVENESS_MAX_S`](_autosummary/an.render.html.md#an.render.UNKNOWN_LIVENESS_MAX_S) | Where a run's process cannot be asked whether it lives (Windows), a run unfinished after this long is taken for one that crashed: otherwise it would shield every cache entry written since, from `an cache gc`, for ever. |

### Functions

| [`cache_entries`](_autosummary/an.render.html.md#an.render.cache_entries)(project, engine, \*\*knobs)    | The shot-cache entry ids of [`cache_reach()`](_autosummary/an.render.html.md#an.render.cache_reach) (what `an.build.gc` keeps of the shot cache, an#274).                                                                                                                                                                                                                                                                                                            |
|-----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`cache_reach`](_autosummary/an.render.html.md#an.render.cache_reach)(project, engine, \*[, fps, ...]) | What a render of `project`'s CURRENT scene under these knobs would read — the shot-cache entry ids, and the record-store entries of the renderers' derived stores (a Manim shot's measurement record, which names its picture and contact sheet: [`an.build.derived`](_autosummary/an.build.derived.html.md#module-an.build.derived), an#299) — computed by the render's own setup and the engine's own key code, rendering and synthesising nothing. |
| [`format_render_findings`](_autosummary/an.render.html.md#an.render.format_render_findings)(project[, ...])       | `an render`'s summary of what the render found: one heading per kind ([`FINDING_GROUPS`](_autosummary/an.render.html.md#an.render.FINDING_GROUPS)) with its count, then each finding's IR path and message — the message carries its fix — at most `max_per_group` per kind.                                                                                                                                                                                            |
| [`live_runs`](_autosummary/an.render.html.md#an.render.live_runs)(project_root)                      | Every cached render of this project still in progress, with the time it started (its live marker's mtime): what `an cache gc` must not race.                                                                                                                                                                                                                                                                                                                |
| [`portable_text`](_autosummary/an.render.html.md#an.render.portable_text)(text, \*[, root, home, tmp])   | `text` with this machine's absolute paths taken out: a path under the project `root` becomes project-relative, the root itself `.`, a temp folder `<tmp>` and the home directory `~` — so a render report (which a project may commit or share, and an agent may pass on) names no user, host folder or temp dir.                                                                                                                                           |
| [`render`](_autosummary/an.render.html.md#an.render.render)(project, \*[, output_name, fps, ...]) | Lower-level: render a loaded `Project` to mp4.                                                                                                                                                                                                                                                                                                                                                                                                              |
| [`render_findings`](_autosummary/an.render.html.md#an.render.render_findings)(project[, output_name])      | The `Finding` s the last render of `output_name` reported (an#254), from `render_reports/<output_name>.json`; `[]` before any render.                                                                                                                                                                                                                                                                                                                       |
| [`render_project`](_autosummary/an.render.html.md#an.render.render_project)(project_dir, \*[, ...])       | Render every shot in `project_dir`'s scene and concatenate to one mp4.                                                                                                                                                                                                                                                                                                                                                                                      |

### Classes

| [`CacheReach`](_autosummary/an.render.html.md#an.render.CacheReach)([ids, derived])   | [`cache_reach()`](_autosummary/an.render.html.md#an.render.cache_reach)'s answer: shot-cache ids, and derived-store entries by store name.   |
|-------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|

### Exceptions

| [`RenderError`](_autosummary/an.render.html.md#an.render.RenderError)   | Raised on render-pipeline failures with actionable detail.   |
|----------------------------------------------------------------|--------------------------------------------------------------|

### *class* an.render.CacheReach(ids=<factory>, derived=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

[`cache_reach()`](_autosummary/an.render.html.md#an.render.cache_reach)’s answer: shot-cache ids, and derived-store entries
by store name.

### an.render.FINDING_GROUPS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'CaptionTimingWarning': 'captions without word timings', 'CutoutAssetWarning': 'art that could not be staged', 'CutoutCompileWarning': 'stand-ins, substitutions and compile notes', 'ShotCacheWarning': 'the shot cache', 'TakeDigestWarning': 'takes whose audio is not the recorded one', 'VoiceLoudnessWarning': 'voices at very different loudness', 'VoiceStandInWarning': 'voices spoken by another provider than they declare', 'dialogue_fits': 'dialogue that does not fit its shot', 'dialogue_in_dissolve': 'dialogue heard during a dissolve', 'library_pins': 'library pins that disagree with assets.lock.json', 'measurement': 'shots whose renderer measured their length'}*

How `an render`’s summary heads each `kind` of finding, in this order; a
kind not listed (another warning category) is headed by its own name, after.

### an.render.POSIX_TEMP_DIRS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('/tmp', '/private/tmp', '/var/tmp')*

The temp folders every POSIX machine has, besides the one Python reports.

### an.render.RENDER_RUNS_DIR *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'runs'*

one directory per CACHED render run.

* **Type:**
  Under `.an/render_work/`

### an.render.RUN_DONE_MARKER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '.done'*

written when the run delivered its film.

* **Type:**
  In a run directory

### an.render.RUN_LIVE_MARKER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '.live'*

the pid of the process rendering it (written at start).

* **Type:**
  In a run directory

### *exception* an.render.RenderError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised on render-pipeline failures with actionable detail.

### an.render.SUMMARY_MAX_PER_GROUP *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 5*

At most this many findings of one kind are listed in the summary.

### an.render.TMP_TOKEN *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '<tmp>'*

What a temp-folder path becomes in a portable text ([`portable_text()`](_autosummary/an.render.html.md#an.render.portable_text)).

### an.render.UNKNOWN_LIVENESS_MAX_S *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 86400.0*

Where a run’s process cannot be asked whether it lives (Windows), a run
unfinished after this long is taken for one that crashed: otherwise it would
shield every cache entry written since, from `an cache gc`, for ever.

### an.render.cache_entries(project, engine, \*\*knobs)

The shot-cache entry ids of [`cache_reach()`](_autosummary/an.render.html.md#an.render.cache_reach) (what `an.build.gc`
keeps of the shot cache, an#274).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.render.cache_reach(project, engine, , fps=None, resolution=None, strict_assets=False, supersample=1, pix_fmt=None, capture=None, step_hz=None, tts=None, lipsync='offline', language='en')

What a render of `project`’s CURRENT scene under these knobs would
read — the shot-cache entry ids, and the record-store entries of the
renderers’ derived stores (a Manim shot’s measurement record, which names
its picture and contact sheet: [`an.build.derived`](_autosummary/an.build.derived.html.md#module-an.build.derived), an#299) — computed
by the render’s own setup and the engine’s own key code, rendering and
synthesising nothing.

What `an.build.gc` keeps (an#274). The dialogue is stamped the way the
render’s audio pipeline stamps it, from the content-keyed audio and viseme
stores only (`an.audio.pipeline.stamp_from_stores`): a `scene.md` edit
drops every stamp on re-sync, and the next render re-stamps the same audio
from the stores, so those are the keys it will use. A line the stores
cannot answer whose provider is free and repeatable (offline speech) is
re-made IN MEMORY, writing nothing (an#311): a later render re-makes the
same bytes. Any other (new text in a billed voice, a non-repeatable
provider) raises `an.audio.pipeline.AudioNotCachedError`: its shot’s next
key is unknowable without a paid or random synthesis, and a collector must
not guess.

* **Return type:**
  [`CacheReach`](_autosummary/an.render.html.md#an.render.CacheReach)

### an.render.format_render_findings(project, output_name='main', , max_per_group=5)

`an render`’s summary of what the render found: one heading per kind
([`FINDING_GROUPS`](_autosummary/an.render.html.md#an.render.FINDING_GROUPS)) with its count, then each finding’s IR path and
message — the message carries its fix — at most `max_per_group` per kind.
`info` findings are counted in the report, not listed. `[]` when the
render found nothing to warn about.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> recs = {"findings": [{"severity": "warning", "ir_path": "timeline/0/dialogue/1",
...     "description": "line 1 (bob) ends at 3.64s as synthesized, past the shot's "
...     "3.6s end. Lengthen the shot", "suggested_fix": None, "location": None,
...     "kind": "dialogue_fits"}]}
>>> import json
>>> print("\n".join(format_render_findings(
...     {"render_reports": {"main": json.dumps(recs)}})))
findings: 1 warning (all in artifacts/render_reports/main.json)
  dialogue that does not fit its shot (1):
    timeline/0/dialogue/1: line 1 (bob) ends at 3.64s as synthesized, past the shot's 3.6s end. Lengthen the shot
```

### an.render.live_runs(project_root)

Every cached render of this project still in progress, with the time it
started (its live marker’s mtime): what `an cache gc` must not race.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### an.render.portable_text(text, , root=None, home=None, tmp=None)

`text` with this machine’s absolute paths taken out: a path under the
project `root` becomes project-relative, the root itself `.`, a temp
folder `<tmp>` and the home directory `~` — so a render report (which a
project may commit or share, and an agent may pass on) names no user, host
folder or temp dir. Only WHOLE path components are replaced, at both ends
(an#309): a sibling that shares a prefix (`/u/me/proj2` beside
`/u/me/p`) or a path that merely ends like one (`/mnt/data/p` against
`/data/p`) is left as it is. A Windows path matches with either
separator and in any case.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> portable_text("missing at /u/me/p/assets/a.png; see /u/me/x.log",
...               root="/u/me/p", home="/u/me", tmp="/t")
'missing at assets/a.png; see ~/x.log'
>>> portable_text("rendered in /u/me/p", root="/u/me/p", home="/u/me", tmp="/t")
'rendered in .'
>>> portable_text("/u/me/proj2/a.png, /u/me2/x, /t/f.png, /mnt/u/me/p/b",
...               root="/u/me/p", home="/u/me", tmp="/t")
'~/proj2/a.png, /u/me2/x, <tmp>/f.png, /mnt/u/me/p/b'
>>> portable_text("c:/users/me/p/a.png", root=r"C:\Users\me\p", home=r"C:\Users\me", tmp="/t")
'a.png'
```

### an.render.render(project, , output_name='main', fps=None, resolution=None, auto_audio=True, tts=None, lipsync='offline', parallel=None, strict_assets=False, supersample=1, pix_fmt=None, capture=None, step_hz=None, language='en', incremental=False, force_render=False, echo_warnings=True)

Lower-level: render a loaded `Project` to mp4.

`incremental` is the build-cache seam (ADR 0004 decision 5): `True` is
the built-in [`ShotCache`](_autosummary/an.build.html.md#an.build.ShotCache) over `mall["shot_cache"]`, an
[`IncrementalEngine`](_autosummary/an.build.html.md#an.build.IncrementalEngine) is used as given, and `False` — the
default HERE, unlike [`render_project()`](_autosummary/an.render.html.md#an.render.render_project) — renders every shot cold, as
this function always has. Cold is this layer’s default because its other
callers are measurements (the bench, the golden corpus, the demo builds),
whose wall times and lever rebinds a reused shot would silently void.
`force_render=True` with an engine renders every shot and re-records it.

`supersample` renders at N times the declared resolution and resolves back
with an exact N x N block mean, in the frame stage, before anything else
reads the frames. **Opt-in, and 1 costs nothing**: at 1 Chromium writes
straight to disk and no pixel is decoded.

**Measured on the shipped path, not on the render alone** — the distinction
matters, because the two differ by 1.6x. `single_character` forced to
1920x1080, 60 frames, this machine:

|   factor |   ms/frame | vs k=1    |
|----------|------------|-----------|
|        1 |      125.5 | 1.00x     |
|        2 |      508.6 | **4.05x** |

`misc/docs/wave3_research.md` §3b reports 2.54x for k=2; that is the
**render only**, measured with a patched runtime and no Python-side resolve,
and quoting it here would understate what a caller pays by 1.6x. The
difference is the decode + block mean + re-encode per frame.

**What it buys, and where it does not.** Research §3a renders each corpus
scene at rising k and lets `edge_transition_width` converge: k=2 travels 57%
to 112% of the way to that ceiling, and k=3 reaches it on every scene that
has one, at twice k=2’s cost. `promote_demo` — the descriptor path — is the
scene it helps most (-34.8% edge width, because the SVG sprite rasterises AT
2x instead of being stretched up from a 1x texture). `aa_probe` has no
ceiling at all: its diagonals land the block-mean grid differently at every
k, so it oscillates +/-5-8% with no settling.

**The corpus cannot inform the factor and must not be used to.** At 320x240
the same ladder reads 1.0x / 1.08x, because fixed costs dominate.

When `auto_audio` is True (the default) and any shot has dialogue,
the audio pipeline is run first so visemes + audio are available to
the renderer. Re-synthesis is triggered on provider changes (the
pipeline’s idempotency check compares against the current providers’
expected content hashes).

`strict_assets=True` refuses to draw a stand-in for a declared asset the
stores do not supply — the placeholder rig for a missing character
descriptor, the default backdrop for an unknown environment ref. Use it for
anything that measures pixels: a stand-in renders happily and is a
different picture (an#33).

**What the render learns, it reports** (an#254). Render is when a line’s
real length becomes known, so after synthesis the checks `an validate`
could only estimate run again on the timing the film will mux — the SAME
functions ([`an.ir.validate.post_synthesis_findings()`](_autosummary/an.ir.validate.html.md#an.ir.validate.post_synthesis_findings)): a line past its
shot’s end, a speaker overlapping themself, a line heard during a dissolve.
With them go the clock-owning renderers’ findings (an#279), the scene’s
library pins that disagree with `assets.lock.json`, and every warning
raised while the film was made — a stand-in or a recorded substitution, a
take whose audio is not the one recorded, a caption without word timings —
each addressed to its shot when its message names one. All of it is written
to `render_reports/<output_name>.json` (`kind` says which check),
readable as `Finding` s with [`render_findings()`](_autosummary/an.render.html.md#an.render.render_findings);
[`format_render_findings()`](_autosummary/an.render.html.md#an.render.format_render_findings) is `an render`’s grouped summary of it.
The warnings are still warned, after the render (`echo_warnings=False`:
only reported — what `an render` passes, since it prints the summary; a
render that fails echoes them anyway). What `strict_assets` refuses is
refused where it is found, before a frame is drawn; nothing here is fatal.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.render.render_findings(project, output_name='main')

The `Finding` s the last render of `output_name` reported (an#254),
from `render_reports/<output_name>.json`; `[]` before any render.

`project` is a project directory, a loaded `Project` or its mall.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### an.render.render_project(project_dir, , output_name='main', fps=None, resolution=None, tts=None, lipsync='offline', parallel=None, strict_assets=False, supersample=1, pix_fmt=None, capture=None, step_hz=None, language='en', incremental=True, force_render=False, echo_warnings=True)

Render every shot in `project_dir`’s scene and concatenate to one mp4.

**Incremental by default** (ADR 0004): a shot whose key — a digest of
everything its render reads, never its id — already has an entry in the
project’s shot cache is not rendered again; its cached mp4 is reused. So
editing one shot re-renders that shot, and an unchanged project re-renders
nothing. `force_render=True` renders every shot anyway (and refreshes
their entries); `incremental=False` neither reads nor writes the cache.
Pass your own engine (e.g. `ShotCache()`) to read what happened to each
shot afterwards from its `report` — the same summary is logged on the
`an.build` logger. See [`render()`](_autosummary/an.render.html.md#an.render.render).

`tts` and `lipsync` may be provider name strings (`"offline"`,
`"elevenlabs"`, `"rhubarb"`) or provider instances. **\`\`tts\`\` defaults
to each voice’s own provider** (an#305): a line whose voice document
declares `provider: elevenlabs` is spoken by ElevenLabs — its cost
announced before the first request, a cached line never billed — and a
voice that declares none by the offline provider, so a project that
declares no provider needs no API key and renders exactly as before. A
`tts` given overrides every voice; a line spoken by another provider than
its voice declares (`--tts offline` for an ElevenLabs voice: silence) is a
finding, and refused under `strict_assets`. `lipsync` defaults to
offline. Switching providers triggers a re-synthesis on dialogue lines whose
stamped audio_ref / viseme_ref no longer match the current configuration.

`parallel` controls per-shot concurrency:

- `None` or `1` (default): render shots serially.
- `"auto"`: `min(n_shots, cpu_count(), DEFAULT_PARALLEL_CAP)`.
- integer ≥ 2: cap the thread pool at that size.

Each shot’s renderer runs in its own thread (the cutout backend
spawns a Chromium + http.server per shot, so threads release the
GIL during the slow parts).

`supersample` renders at N times the declared resolution and resolves back
with an exact block mean. **Opt-in, and 1 is free** — at 1 nothing is
decoded and Chromium’s own bytes reach disk. See [`render()`](_autosummary/an.render.html.md#an.render.render).

`capture` picks how frames leave the browser: `"canvas"` (the default,
via `None`, since an#192) — an in-page read of the canvas, batched,
writing frames whose decoded pixels equal the screenshot path’s and
measured ~7.8x faster in the frame stage on the golden corpus, ~2.3x at
1080p (see `an.stage.canvas_capture`) — or `"screenshot"`, a
Playwright element screenshot per instant.

`step_hz` overrides the scene’s `meta.step_hz` for this render (a shot’s
own `step_hz` still wins): authored tweens are resampled onto a pose grid
of that many updates per second — 15 at 30 fps is “on twos”. `None` uses
the scene’s declaration, which is itself `None` (smooth) by default.

`language` (BCP-47) reaches lip-sync providers that select behaviour by
it when `lipsync` is a provider *name* — Rhubarb’s recognizer (an#96). A
provider *instance* carries its own.

A scene whose `library:` pins disagree with the project’s
`assets.lock.json` renders with a `LibraryPinWarning` per pin, and is
refused under `strict_assets` (`an.library.checkout.check_pins_before_render()`).

**What the render learned is reported** (an#254): see [`render()`](_autosummary/an.render.html.md#an.render.render) — every
finding is in `render_reports/<output_name>.json`, read back as
`Finding` s by [`render_findings()`](_autosummary/an.render.html.md#an.render.render_findings), and summarised by `an render`.

Returns the absolute path of the final output file (under `output/`).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.semantic.describe.html.md

# an.semantic.describe

Describe an asset: what it affords, and per aspect what applies and what is missing.

ADR 0002’s first slice ends with `an character capabilities <name>`: the
affordances (with the declared overrides the derivation used), and per aspect
the method the default chain picks, the methods that apply, and the
`why_not` of the rest with their remedies. [`describe_asset()`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_asset) is that
answer as data (the MCP surface’s describe-an-asset query returns it);
[`format_description()`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.format_description) is its terminal form. Core code: it names no rig.
Given a `policy` (a style’s, an#348), each aspect also says what it
resolves to under it (`under_policy`), by the same precedence the compiler
uses ([`an.semantic.resolve()`](_autosummary/an.semantic.html.md#an.semantic.resolve)).

```pycon
>>> from an.semantic.entries import Aspect, Method
>>> from an.semantic.registry import register_aspect, register_entry, drop_owner
>>> _ = register_entry(Method("demo.fly", aspect="demo_travel", requires=("limbs.wings",)), owner="demo")
>>> _ = register_entry(Method("demo.float", aspect="demo_travel"), owner="demo")
>>> _ = register_aspect(Aspect("demo_travel", chain=("demo.fly", "demo.float")), owner="demo")
>>> d = describe_profile({}, aspects=("demo_travel",))
>>> d["aspects"]["demo_travel"]["default"], list(d["aspects"]["demo_travel"]["not_applicable"])
('demo.float', ['demo.fly'])
>>> d = describe_profile({}, aspects=("demo_travel",), policy={"demo_travel": ["demo.fly", "demo.float"]})
>>> u = d["aspects"]["demo_travel"]["under_policy"]
>>> u["method"], u["source"], u["skipped"]
('demo.float', 'policy', [{'method': 'demo.fly', 'missing': ['limbs.wings']}])
>>> drop_owner("demo")
```

### Functions

| [`describe_asset`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_asset)(doc[, art, kind, aspects, policy])   | [`describe_profile()`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_profile) of an asset's derived profile, with the analyser and overrides; `policy` as [`describe_profile()`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_profile)'s.   |
|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`describe_profile`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_profile)(profile, \*[, kind, ...])          | Per aspect: the method it resolves to, the applicable ones, and why not the rest.                                                                                                                                        |
| [`format_description`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.format_description)(d, \*[, name, policy_label])     | The terminal form of [`describe_asset()`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_asset); `policy_label` names the policy an `under_policy` answer was resolved under (a style's name).                                    |

### an.semantic.describe.describe_asset(doc, art=None, , kind='character', aspects=None, policy=None)

[`describe_profile()`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_profile) of an asset’s derived profile, with the analyser and
overrides; `policy` as [`describe_profile()`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_profile)’s.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.semantic.describe.describe_profile(profile, , kind=None, aspects=None, declared=None, policy=None)

Per aspect: the method it resolves to, the applicable ones, and why not the rest.

`declared` is the asset document’s declared facts: an aspect’s
declaration is read from the field it names (`Aspect.declared_by`: a
character’s `gait`), so the answer is the method the compiler will use.
`policy` (a style’s, anything `an.semantic.Policy.of()` reads) adds
`under_policy` per aspect: the method, where the choice came from, what
that method requires (a genre may say when it holds), and any
substitution or skipped policy entries.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.semantic.describe.format_description(d, , name='', policy_label='the policy')

The terminal form of [`describe_asset()`](_autosummary/an.semantic.describe.html.md#an.semantic.describe.describe_asset); `policy_label` names the
policy an `under_policy` answer was resolved under (a style’s name).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.semantic.digest.html.md

# an.semantic.digest

Which vocabulary entries a shot names, at which versions: the shot’s vocabulary digest.

ADR 0003 decision 2: the versions of the entries a shot uses are folded into
that shot’s compile key (ADR 0004), so changing what a name means re-renders
the shots that say it — visibly, never silently. [`vocabulary_versions()`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_versions)
walks a shot and collects `{entry id: version}`; [`vocabulary_digest()`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_digest)
hashes it.

**In the shot key.** The content-keyed shot cache (P6, an#242) adds key parts
through `an.build.keys.register_shot_key_part(renderer, name, fn)`; this
digest is the cut-out keyer’s `vocabulary` part
([`register_vocabulary_key_part()`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.register_vocabulary_key_part), called by `an.stage.render`), so
bumping an entry’s version re-renders the shots that use it.

Over-inclusion is deliberate and safe: a `play` of a name that is a motion
preset counts the preset even if the character’s descriptor shadows it, and a
preset that resolves an aspect counts every method of that aspect — a version
bump then re-renders a shot that may not have needed it, never the reverse.

```pycon
>>> from an.ir.schema import Shot, Camera
>>> v = vocabulary_versions(Shot(id="s", camera=Camera(move="push_in")))
>>> v["camera.push_in"]
'1'
```

### Module Attributes

| [`VOCABULARY_KEY_PART`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.VOCABULARY_KEY_PART)   | The name of the shot-key part this digest is folded in under.   |
|------------------------------------------------------------------------|-----------------------------------------------------------------|

### Functions

| [`register_vocabulary_key_part`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.register_vocabulary_key_part)([renderer])   | Fold [`vocabulary_digest()`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_digest) into `renderer`'s shot key, through P6's seam.   |
|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| [`vocabulary_key_part`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_key_part)(shot[, ctx])           | The shot-key part (`an.build.keys.ShotKeyPart`): the shot's vocabulary digest.                                             |
| [`vocabulary_digest`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_digest)(shot)                    | The sha256 of [`vocabulary_versions()`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_versions) — one more part of a shot's key.      |
| [`vocabulary_versions`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_versions)(shot)                  | `{entry id: version}` for every registered entry `shot` names, sorted.                                                     |

### an.semantic.digest.VOCABULARY_KEY_PART *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'vocabulary'*

The name of the shot-key part this digest is folded in under.

### an.semantic.digest.register_vocabulary_key_part(renderer='cutout')

Fold [`vocabulary_digest()`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_digest) into `renderer`’s shot key, through P6’s seam.

Idempotent, and tolerant of order: returns `False` (registering nothing)
when `renderer` has no shot keyer yet, `True` once the part is in —
called again, it leaves the registered part alone. The cut-out adapter
calls it right after registering its keyer (`an.stage.render`, loaded
lazily by the renderer registry, which `shot_keyer_for` consults first).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> register_vocabulary_key_part(), register_vocabulary_key_part("no-such-renderer")
(True, False)
```

### an.semantic.digest.vocabulary_digest(shot)

The sha256 of [`vocabulary_versions()`](_autosummary/an.semantic.digest.html.md#an.semantic.digest.vocabulary_versions) — one more part of a shot’s key.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.semantic.digest.vocabulary_key_part(shot, ctx=None)

The shot-key part (`an.build.keys.ShotKeyPart`): the shot’s vocabulary digest.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.semantic.digest.vocabulary_versions(shot)

`{entry id: version}` for every registered entry `shot` names, sorted.

Names the registry does not know (a descriptor animation, a parametrised
easing) are not entries and add nothing; their effect is in the compiled
document, which the shot key already covers.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/an.semantic.docs.html.md

# an.semantic.docs

The vocabulary section of the downstream `an` skill, generated from the registry.

ADR 0003 decision 6’s third generated surface. The skill keeps its prose; the
table between the two markers is rendered here and checked by a test, so a
preset, a method or a version cannot change without the skill saying so:

```default
python -m an.semantic.docs --write .claude/skills/an/SKILL.md
```

Each owner renders only its own rows (an#354): `an`’s skill carries the core
vocabulary, and a genre keeps its rows in its own skill, regenerated and checked
there with `--owner <genre>`, so a genre change cannot turn `an` red.

```pycon
>>> "| `walk` |" in skill_vocabulary_section() or "| `tween` |" in skill_vocabulary_section()
True
```

### Functions

| [`replace_section`](_autosummary/an.semantic.docs.html.md#an.semantic.docs.replace_section)(text, section)        | `text` with the block between the markers replaced by `section`.                  |
|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`skill_vocabulary_section`](_autosummary/an.semantic.docs.html.md#an.semantic.docs.skill_vocabulary_section)(\*[, owner]) | The markdown between the markers: one table per kind, then the methods by aspect. |

### an.semantic.docs.replace_section(text, section)

`text` with the block between the markers replaced by `section`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> replace_section(f"a\n{BEGIN_MARKER}\nold\n{END_MARKER}\nb", "new").splitlines()[2]
'new'
```

### an.semantic.docs.skill_vocabulary_section(, owner='an')

The markdown between the markers: one table per kind, then the methods by aspect.

Only the rows `owner` registered: the core by default, a genre’s own with
`owner=<genre>` (its skill points back here for the core rows).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.semantic.export.html.md

# an.semantic.export

The vocabulary as a published, code-free contract file: `an/data/timing/vocabulary.json`.

ADR 0006 decision 2 (and an#261): the vocabulary registry
([`an.semantic.registry`](_autosummary/an.semantic.registry.html.md#module-an.semantic.registry)) is authored in `an`; any other engine — the
TypeScript `previz`, `shaping`, the `burns` port — reads it as data. This
module is the exporter, in the pattern of [`an.timing.contract`](_autosummary/an.timing.contract.html.md#module-an.timing.contract): a builder
([`build_vocabulary()`](_autosummary/an.semantic.export.html.md#an.semantic.export.build_vocabulary)), a writer ([`write_vocabulary()`](_autosummary/an.semantic.export.html.md#an.semantic.export.write_vocabulary)), a drift check
([`vocabulary_drift()`](_autosummary/an.semantic.export.html.md#an.semantic.export.vocabulary_drift)) and a `write`/`check` command line:

```default
python -m an.semantic.export write
python -m an.semantic.export check
```

What it carries, per entry: `id`, `kind`, `name` (how a document spells
it), `version`, `title`, `description`, `levels` (the spectrum levels it
accepts), `params` (a JSON Schema object with defaults), `requires` (the
ADR 0002 requirement terms) and, where the entry has them, `usage`,
`examples`, `aspects` and (methods) `aspect` and `remedies`. Never
`expand`: no code leaves `an`. Camera moves are entries of kind
`camera_move` whose `params.path` lies in a `view_space` entry
(`view.framing2d`, `view.orbit3d`), so an engine reads a move once and
lowers it through the space it affords.

\*\*Only `an`’s own entries\*\* (`owner="an"`) are exported, so the file does
not depend on which genres happen to be loaded, and an installed genre never
edits it. Publishing a genre’s entries is a deliberate act: pass `owners=` and
say so (ADR 0006, Risks: the package is public).

```pycon
>>> doc = build_vocabulary()
>>> doc["version"], doc["owners"]
(1, ['an'])
>>> by_id = {e["id"]: e for e in doc["entries"]}
>>> by_id["camera.push_in"]["params"]["properties"]["path"]["default"][-1]["zoom"] > 1
True
>>> "expand" in by_id["camera.push_in"], by_id["view.framing2d"]["kind"]
(False, 'view_space')
```

### Module Attributes

| [`VOCABULARY_FILE`](_autosummary/an.semantic.export.html.md#an.semantic.export.VOCABULARY_FILE)           | The vocabulary export's file name, beside the timing contract's files.      |
|----------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`VOCABULARY_FORMAT_VERSION`](_autosummary/an.semantic.export.html.md#an.semantic.export.VOCABULARY_FORMAT_VERSION) | The shape of the file (not of its entries, which carry their own versions). |

### Functions

| [`build_vocabulary`](_autosummary/an.semantic.export.html.md#an.semantic.export.build_vocabulary)(\*[, owners])   | The export document: every entry `owners` registered, as data, sorted by id.                |
|-----------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| [`vocabulary_drift`](_autosummary/an.semantic.export.html.md#an.semantic.export.vocabulary_drift)([directory])    | How the committed `vocabulary.json` differs from what the registry generates (empty: none). |
| [`write_vocabulary`](_autosummary/an.semantic.export.html.md#an.semantic.export.write_vocabulary)([directory])    | Regenerate `vocabulary.json` under `directory`.                                             |

### an.semantic.export.VOCABULARY_FILE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'vocabulary.json'*

The vocabulary export’s file name, beside the timing contract’s files.

### an.semantic.export.VOCABULARY_FORMAT_VERSION *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 1*

The shape of the file (not of its entries, which carry their own versions).

### an.semantic.export.build_vocabulary(, owners=('an',))

The export document: every entry `owners` registered, as data, sorted by id.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.semantic.export.vocabulary_drift(directory=PosixPath('/home/runner/work/an/an/an/data/timing'))

How the committed `vocabulary.json` differs from what the registry generates (empty: none).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.semantic.export.write_vocabulary(directory=PosixPath('/home/runner/work/an/an/an/data/timing'))

Regenerate `vocabulary.json` under `directory`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.semantic.html.md

# an.semantic

The semantic layer: one versioned vocabulary registry, methods, aspects and the matcher.

ADR 0003 (the structured ↔ semantic spectrum) and ADR 0002 (capability-based
applicability). Defined once, in the core, for every genre (design principle 3):

- **the vocabulary** — every name a document may spell, as an [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry)
  (id, version, kind, title, description, params as JSON Schema with
  defaults, examples, requires, accepted levels, expand): action and entity
  kinds, easings, camera moves and IR fields from the core
  ([`an.semantic.seeds`](_autosummary/an.semantic.seeds.html.md#module-an.semantic.seeds)); motion and expression presets, methods and
  aspects from the genres ([`an.genres.Genre`](_autosummary/an.genres.html.md#an.genres.Genre)’s `vocabulary` and
  `aspects`);
- **methods and aspects** — a [`Method`](_autosummary/an.semantic.html.md#an.semantic.Method) is an entry of kind `method`
  realising an [`Aspect`](_autosummary/an.semantic.html.md#an.semantic.Aspect), whose default chain ends in a method that
  requires nothing or in the recorded `NOOP`;
- **the queries** — [`applicable()`](_autosummary/an.semantic.html.md#an.semantic.applicable), [`why_not()`](_autosummary/an.semantic.html.md#an.semantic.why_not) and [`resolve()`](_autosummary/an.semantic.html.md#an.semantic.resolve)
  (with a [`Policy`](_autosummary/an.semantic.html.md#an.semantic.Policy)), all calls to the one matcher
  [`an.capabilities.missing()`](_autosummary/an.capabilities.html.md#an.capabilities.missing);
- **the generated surfaces** — the `an iterate` prompt
  ([`an.semantic.prompt`](_autosummary/an.semantic.prompt.html.md#module-an.semantic.prompt)), the skill’s vocabulary section
  ([`an.semantic.docs`](_autosummary/an.semantic.docs.html.md#module-an.semantic.docs)), the MCP surface ([`an.mcp`](_autosummary/an.mcp.html.md#module-an.mcp)), and the shot’s
  vocabulary digest for the compile key ([`an.semantic.digest`](_autosummary/an.semantic.digest.html.md#module-an.semantic.digest)).

Nothing here imports `an.ir` at module level, and nothing below the IR ever
calls an LLM.

```pycon
>>> from an.semantic import entry, vocabulary
>>> entry("action.tween").kind
'action'
>>> any(e["id"] == "easing.ease_in_out" for e in vocabulary())
True
```

### Functions

| [`applicable`](_autosummary/an.semantic.html.md#an.semantic.applicable)(aspect_name, subjects)                | The methods of `aspect_name` that apply to `subjects`, the default chain's order first.                                    |
|---------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| [`aspect`](_autosummary/an.semantic.html.md#an.semantic.aspect)(name)                                     | The registered aspect `name`; [`UnknownEntryError`](_autosummary/an.semantic.html.md#an.semantic.UnknownEntryError) otherwise.                |
| `aspect_names`(\*[, owner])                                                                       |                                                                                                                            |
| `aspects`()                                                                                       |                                                                                                                            |
| [`check_registry`](_autosummary/an.semantic.html.md#an.semantic.check_registry)(\*[, owner, capabilities])        | The problems with the registered aspects and methods (empty: sound).                                                       |
| [`entries`](_autosummary/an.semantic.html.md#an.semantic.entries)(\*[, kind, owner])                       | Every entry (registered and viewed), filtered by `kind`/`owner`, in a stable order.                                        |
| [`entry`](_autosummary/an.semantic.html.md#an.semantic.entry)(entry_id)                                  | The entry with this id; [`UnknownEntryError`](_autosummary/an.semantic.html.md#an.semantic.UnknownEntryError) naming the known ids otherwise. |
| [`lookup`](_autosummary/an.semantic.html.md#an.semantic.lookup)(kind, name, \*[, aspect])                 | The entry of `kind` a document spells `name` (for methods, within `aspect`).                                               |
| [`methods_of`](_autosummary/an.semantic.html.md#an.semantic.methods_of)(aspect_name)                          | Every registered method of an aspect: its chain first, in order, then the rest.                                            |
| `owner_of`(entry_id)                                                                              |                                                                                                                            |
| [`register_aspect`](_autosummary/an.semantic.html.md#an.semantic.register_aspect)(a, \*[, owner, replace])         | Register an aspect and its default chain.                                                                                  |
| [`register_entry`](_autosummary/an.semantic.html.md#an.semantic.register_entry)(e, \*[, owner, replace])          | Register a vocabulary entry under `owner`: ONE entry per id, and per spelling.                                             |
| [`resolve`](_autosummary/an.semantic.html.md#an.semantic.resolve)(aspect_name, subjects[, requested, ...]) | Choose the method that realises `aspect_name` on `subjects`.                                                               |
| [`vocabulary`](_autosummary/an.semantic.html.md#an.semantic.vocabulary)(\*[, kind, owner])                    | Every entry as data (what the MCP surface returns), filtered by `kind`/`owner`.                                            |
| [`why_not`](_autosummary/an.semantic.html.md#an.semantic.why_not)(method, subjects)                        | What `method` is missing on `subjects`, each with its remedy (empty: it applies).                                          |

### Classes

| [`Aspect`](_autosummary/an.semantic.html.md#an.semantic.Aspect)(name, chain[, applies_to, ...])          | Something every asset of a kind gets (locomotion, speech, blink, …).                                                     |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| [`Choice`](_autosummary/an.semantic.html.md#an.semantic.Choice)(method[, args, version])                 | One method choice in a policy or a request: `previz`'s `FormulaCall` plus a pin.                                         |
| [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry)(id, kind[, version, name, title, ...])    | One vocabulary entry: a named, versioned, described, parametrised recipe.                                                |
| [`Method`](_autosummary/an.semantic.html.md#an.semantic.Method)(id[, kind, version, name, title, ...])   | A way of realising an aspect (ADR 0002): an entry of kind `method`.                                                      |
| [`Missing`](_autosummary/an.semantic.html.md#an.semantic.Missing)(term, remedy)                           | One unmet requirement term and what would meet it.                                                                       |
| [`Policy`](_autosummary/an.semantic.html.md#an.semantic.Policy)([order])                                 | Per-aspect method orders set by a style (study_the_masters §4).                                                          |
| [`Resolution`](_autosummary/an.semantic.html.md#an.semantic.Resolution)(aspect, method[, args, source, ...]) | What [`resolve()`](_autosummary/an.semantic.html.md#an.semantic.resolve) chose: the method, its args, where the choice came from. |

### Exceptions

| [`UnknownEntryError`](_autosummary/an.semantic.html.md#an.semantic.UnknownEntryError)   | A name or id is not in the vocabulary; the message lists what is.                    |
|----------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`VocabularyError`](_autosummary/an.semantic.html.md#an.semantic.VocabularyError)     | A vocabulary entry, aspect or policy is malformed or collides with a registered one. |

### *class* an.semantic.Aspect(name, chain, applies_to=frozenset({}), description='', declared_by='', records_fallback=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Something every asset of a kind gets (locomotion, speech, blink, …).

`chain` is the default order of method ids, first applicable wins; its
last link requires nothing, or is `NOOP`. `applies_to` is the
entity kinds it makes sense for (empty: all); for any other kind the
aspect resolves to the recorded no-op.

#### declared_by *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= ''*

The asset-document field an asset declares its request in (a
character’s `gait`, `speech`): a declared method choice, reported as
such by `describe_asset` and honoured by the compiler.

#### records_fallback *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= False*

Record falling down the chain even when nothing was requested (reason
`missing`, so `--strict-assets` sees it). For an aspect whose
fallback is a behaviour the asset never had before — speech’s pulse on a
baked face — rather than today’s long-standing default (a legless walk).

### *class* an.semantic.Choice(method, args=<factory>, version=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One method choice in a policy or a request: `previz`’s `FormulaCall` plus a pin.

```pycon
>>> Choice.of({"method": "speech.mouth_flap", "args": {"shapes": 3}}).args
{'shapes': 3}
>>> Choice.of("loco.rock").method
'loco.rock'
```

### *class* an.semantic.Entry(id, kind, version='1', name='', title='', description='', usage='', params=<factory>, examples=(), requires=(), levels=frozenset({'a', 'b-name'}), aspects=(), expand=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One vocabulary entry: a named, versioned, described, parametrised recipe.

- `id` is the persisted identifier (dotted: `motion.walk`,
  `loco.legged_cycle`); `name` is how a document spells it (`walk`;
  defaults to `id`), looked up per `kind`.
- `version` changes whenever the meaning changes for the same params; the
  versions of the entries a shot uses fold into its compile key.
- `params` is a JSON Schema object; `properties.*.default` are the
  defaults (`{}` when the entry takes none).
- `aspects` names the aspects that using this entry resolves (a `walk`
  resolves `locomotion`), so the digest of a shot can include the methods
  it may resolve to.
- `usage` is the longer note the generated surfaces print under the
  one-sentence `description` (how a document spells it, what bites).

#### defaults()

The params’ defaults (`properties.*.default`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

#### *classmethod* from_json(data, , expand=None)

An entry from its JSON form (what [`to_json()`](_autosummary/an.semantic.html.md#an.semantic.Entry.to_json) writes, or another
package exports — previz’s formulas): the explicit loader, a `method`
kind giving a [`Method`](_autosummary/an.semantic.html.md#an.semantic.Method).

* **Return type:**
  [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry)

```pycon
>>> e = Entry("camera.demo", "camera_move", name="demo", description="d")
>>> Entry.from_json(e.to_json()) == e
True
```

#### *property* term *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

How a document spells this entry.

#### to_json()

The entry as data (no `expand`): what the MCP surface and the docs list.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* an.semantic.Method(id, kind='method', version='1', name='', title='', description='', usage='', params=<factory>, examples=(), requires=(), levels=frozenset({'a', 'b-name'}), aspects=(), expand=None, aspect='', remedies=<factory>)

Bases: [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry)

A way of realising an aspect (ADR 0002): an entry of kind `method`.

`remedies` overrides a capability’s own remedy per requirement term (a
method can say “split the legs into two slots with hip pivots” where the
capability only says “add legs”).

```pycon
>>> m = Method("loco.rock", aspect="locomotion", name="rock")
>>> m.kind, m.aspect, m.requirement_free
('method', 'locomotion', True)
```

#### *property* requirement_free *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether the method applies to anything (a legal last link of a chain).

#### to_json()

The entry as data (no `expand`): what the MCP surface and the docs list.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* an.semantic.Missing(term, remedy)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One unmet requirement term and what would meet it.

### *class* an.semantic.Policy(order=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Per-aspect method orders set by a style (study_the_masters §4).

The parsed form of a style document’s `policy:` block; precedence when
resolving is the author’s request, then the shot, then the style, then the
aspect’s chain ([`layered()`](_autosummary/an.semantic.html.md#an.semantic.Policy.layered)).

```pycon
>>> p = Policy.of({"locomotion": ["loco.bob", {"method": "loco.glide", "args": {"bob": 0}}]})
>>> [c.method for c in p.choices("locomotion")], p.choices("speech")
(['loco.bob', 'loco.glide'], ())
```

#### *static* layered(\*policies)

Policies in precedence order (shot before style): the first that names an aspect wins it.

* **Return type:**
  [`Policy`](_autosummary/an.semantic.html.md#an.semantic.Policy)

### *class* an.semantic.Resolution(aspect, method, args=<factory>, source='chain', substitution=None, considered=(), skipped=(), choice=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What [`resolve()`](_autosummary/an.semantic.html.md#an.semantic.resolve) chose: the method, its args, where the choice came from.

`source` is `request`, `policy`, `chain` or `noop`;
`substitution` is the record when the choice departs from what was asked
(`None` when it did not); `considered` is the trail of methods tried
before it, each with what it was missing; `skipped` the policy’s entries
among them (a policy is an order: an entry that does not apply is passed
over, information for whoever records the choice, never a substitution);
`choice` the request or policy entry that was chosen, as written (its own
`args`, without the method’s defaults), `None` for a chain link.

### *exception* an.semantic.UnknownEntryError

Bases: [`VocabularyError`](_autosummary/an.semantic.html.md#an.semantic.VocabularyError), [`KeyError`](https://docs.python.org/3/builtins/exceptions.html#KeyError)

A name or id is not in the vocabulary; the message lists what is.

### *exception* an.semantic.VocabularyError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A vocabulary entry, aspect or policy is malformed or collides with a registered one.

### an.semantic.applicable(aspect_name, subjects)

The methods of `aspect_name` that apply to `subjects`, the default chain’s order first.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Method`](_autosummary/an.semantic.html.md#an.semantic.Method), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.semantic.aspect(name)

The registered aspect `name`; [`UnknownEntryError`](_autosummary/an.semantic.html.md#an.semantic.UnknownEntryError) otherwise.

* **Return type:**
  [`Aspect`](_autosummary/an.semantic.html.md#an.semantic.Aspect)

### an.semantic.check_registry(, owner=None, capabilities=True)

The problems with the registered aspects and methods (empty: sound).

ADR 0002 decision 5 as a check: every aspect’s chain names methods of that
aspect, and its last link requires nothing or is the recorded `NOOP`;
one spelling is one entry; and (`capabilities`) every requirement names a
registered capability. `owner` limits it to one genre’s aspects and
entries. [`an.genres.register_genre()`](_autosummary/an.genres.html.md#an.genres.register_genre) runs the chain checks per genre;
[`an.genres.load()`](_autosummary/an.genres.html.md#an.genres.load) runs the capability check once every genre is in, so
a genre needing another’s capability does not depend on load order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.semantic.entries(, kind=None, owner=None)

Every entry (registered and viewed), filtered by `kind`/`owner`, in a stable order.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.semantic.entry(entry_id)

The entry with this id; [`UnknownEntryError`](_autosummary/an.semantic.html.md#an.semantic.UnknownEntryError) naming the known ids otherwise.

* **Return type:**
  [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry)

### an.semantic.lookup(kind, name, , aspect=None)

The entry of `kind` a document spells `name` (for methods, within `aspect`).

* **Return type:**
  [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.semantic.methods_of(aspect_name)

Every registered method of an aspect: its chain first, in order, then the rest.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Method`](_autosummary/an.semantic.html.md#an.semantic.Method), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.semantic.register_aspect(a, , owner='an', replace=False)

Register an aspect and its default chain. Checked as a whole by
[`an.semantic.check_registry()`](_autosummary/an.semantic.html.md#an.semantic.check_registry) (a chain may name methods registered
after it, within the same genre).

* **Return type:**
  [`Aspect`](_autosummary/an.semantic.html.md#an.semantic.Aspect)

### an.semantic.register_entry(e, , owner='an', replace=False)

Register a vocabulary entry under `owner`: ONE entry per id, and per spelling.

A name means one thing everywhere (`push_in` is one entry, an#257): a
second entry under a taken id — registered or provided by a view — or under
a taken `(kind, name)` (within an aspect, for methods) raises, unless
`replace=True` is passed explicitly, and then the replacement is recorded
(`replacements()`). Re-registering the same entry is a no-op.

* **Return type:**
  [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry)

```pycon
>>> from an.semantic.entries import Entry
>>> register_entry(Entry("demo.push_in", "camera_move", name="push_in"), owner="demo")
Traceback (most recent call last):
...
an.semantic.entries.VocabularyError: camera_move 'push_in' is already defined by 'camera.push_in' (owner 'an'); ...
```

### an.semantic.resolve(aspect_name, subjects, requested=None, , declared=None, policy=None, entity='', entity_kind=None)

Choose the method that realises `aspect_name` on `subjects`.

`requested` is the author’s explicit choice (a method id, its spelling
in the aspect — `"hem"` for locomotion —, or `{method, args, version}`);
`declared` the asset’s own declaration for the aspect (the field
`Aspect.declared_by` names), placed by `DECLARED_OUTRANKS_POLICY`
(the request when nothing was requested, or the policy’s last entry);
`policy` the layered shot/style policy; `entity_kind` the asset’s kind,
checked against the aspect’s `applies_to`. Never raises for a method that
does not apply: it falls back and records why.

* **Return type:**
  [`Resolution`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.Resolution)

### an.semantic.vocabulary(, kind=None, owner=None)

Every entry as data (what the MCP surface returns), filtered by `kind`/`owner`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.semantic.why_not(method, subjects)

What `method` is missing on `subjects`, each with its remedy (empty: it applies).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Missing`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.Missing), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### Modules

| [`describe`](_autosummary/an.semantic.describe.html.md#module-an.semantic.describe)   | Describe an asset: what it affords, and per aspect what applies and what is missing.        |
|-----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| [`digest`](_autosummary/an.semantic.digest.html.md#module-an.semantic.digest)       | Which vocabulary entries a shot names, at which versions: the shot's vocabulary digest.     |
| [`docs`](_autosummary/an.semantic.docs.html.md#module-an.semantic.docs)           | The vocabulary section of the downstream `an` skill, generated from the registry.           |
| [`export`](_autosummary/an.semantic.export.html.md#module-an.semantic.export)       | The vocabulary as a published, code-free contract file: `an/data/timing/vocabulary.json`.   |
| [`matcher`](_autosummary/an.semantic.matcher.html.md#module-an.semantic.matcher)     | The three queries and the policy (ADR 0002 decision 4): `applicable`, `why_not`, `resolve`. |
| [`prompt`](_autosummary/an.semantic.prompt.html.md#module-an.semantic.prompt)       | The `an iterate` system prompt, generated from the vocabulary registry.                     |
| [`registry`](_autosummary/an.semantic.registry.html.md#module-an.semantic.registry)   | The one vocabulary registry (ADR 0003 decision 6): entries and aspects, by owner.           |
| [`seeds`](_autosummary/an.semantic.seeds.html.md#module-an.semantic.seeds)         | The core's vocabulary: views over the kind and easing registries, camera moves, IR fields.  |
| [`views`](_autosummary/an.semantic.views.html.md#module-an.semantic.views)         | View spaces: what a camera move moves through, defined once for every engine (an#257).      |


# _autosummary/an.semantic.matcher.html.md

# an.semantic.matcher

The three queries and the policy (ADR 0002 decision 4): `applicable`, `why_not`, `resolve`.

All three are calls to the one matcher, [`an.capabilities.missing()`](_autosummary/an.capabilities.html.md#an.capabilities.missing), over
the methods the vocabulary registry holds for an aspect.

- [`applicable()`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.applicable) lists the methods of an aspect that apply to the subjects
  (the default chain’s order first).
- [`why_not()`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.why_not) lists what a method is missing, each term with its remedy —
  the method’s own, else the capability’s.
- [`resolve()`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.resolve) picks one: the author’s request if it applies; else the first
  applicable choice of the policy (shot before style, `Policy.layered()`);
  else the aspect’s default chain. Every departure from what was asked is a
  [`Substitution`](_autosummary/an.capabilities.html.md#an.capabilities.Substitution) on the result: `missing` when what
  > the author asked does not apply, `policy` when a policy chose against the
  > chain (information, never fatal), `noop` when the aspect does not apply at
  > all. A policy is an order, not a request: its entries passed over on the way
  > to its first applicable one are listed in `Resolution.skipped`, never
  > recorded as a `missing` substitution (an#334).

```pycon
>>> from an.semantic.entries import Aspect, Method
>>> from an.semantic.registry import register_aspect, register_entry, drop_owner
>>> _ = register_entry(Method("demo.legs", aspect="demo_move", requires=("limbs.legs",)), owner="demo")
>>> _ = register_entry(Method("demo.slide", aspect="demo_move"), owner="demo")
>>> _ = register_aspect(Aspect("demo_move", chain=("demo.legs", "demo.slide")), owner="demo")
>>> [m.id for m in applicable("demo_move", {})]
['demo.slide']
>>> [w.term for w in why_not("demo.legs", {})]
['limbs.legs']
>>> r = resolve("demo_move", {}, requested="demo.legs", entity="blob")
>>> r.method.id, r.substitution.reason
('demo.slide', 'missing')
>>> drop_owner("demo")
```

### Module Attributes

| [`DECLARED_OUTRANKS_POLICY`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.DECLARED_OUTRANKS_POLICY)   | Does an asset's DECLARED method (a character's `gait` or `speech`, the field its aspect names in `Aspect.declared_by`) outrank a policy? `True` (today's rule): the declaration is the request, so a character keeps its personality across styles, and one it cannot honour is a `missing` substitution.   |
|-----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### Functions

| [`applicable`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.applicable)(aspect_name, subjects)                | The methods of `aspect_name` that apply to `subjects`, the default chain's order first.   |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`resolve`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.resolve)(aspect_name, subjects[, requested, ...]) | Choose the method that realises `aspect_name` on `subjects`.                              |
| [`why_not`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.why_not)(method, subjects)                        | What `method` is missing on `subjects`, each with its remedy (empty: it applies).         |

### Classes

| [`Missing`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.Missing)(term, remedy)                           | One unmet requirement term and what would meet it.                                                                       |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| [`Resolution`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.Resolution)(aspect, method[, args, source, ...]) | What [`resolve()`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.resolve) chose: the method, its args, where the choice came from. |

### an.semantic.matcher.DECLARED_OUTRANKS_POLICY *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

Does an asset’s DECLARED method (a character’s `gait` or `speech`, the
field its aspect names in `Aspect.declared_by`) outrank a policy? `True`
(today’s rule): the declaration is the request, so a character keeps its
personality across styles, and one it cannot honour is a `missing`
substitution. `False` (ADR 0002 read the other way): the declaration is
the LAST entry of the policy’s order, so a style looks like itself and the
declaration is its fallback. The maintainer’s decision is cutan#36; this
flag is the whole switch, for [`resolve()`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.resolve) and every caller of it.

### *class* an.semantic.matcher.Missing(term, remedy)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One unmet requirement term and what would meet it.

### *class* an.semantic.matcher.Resolution(aspect, method, args=<factory>, source='chain', substitution=None, considered=(), skipped=(), choice=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What [`resolve()`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.resolve) chose: the method, its args, where the choice came from.

`source` is `request`, `policy`, `chain` or `noop`;
`substitution` is the record when the choice departs from what was asked
(`None` when it did not); `considered` is the trail of methods tried
before it, each with what it was missing; `skipped` the policy’s entries
among them (a policy is an order: an entry that does not apply is passed
over, information for whoever records the choice, never a substitution);
`choice` the request or policy entry that was chosen, as written (its own
`args`, without the method’s defaults), `None` for a chain link.

### an.semantic.matcher.applicable(aspect_name, subjects)

The methods of `aspect_name` that apply to `subjects`, the default chain’s order first.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Method`](_autosummary/an.semantic.html.md#an.semantic.Method), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.semantic.matcher.resolve(aspect_name, subjects, requested=None, , declared=None, policy=None, entity='', entity_kind=None)

Choose the method that realises `aspect_name` on `subjects`.

`requested` is the author’s explicit choice (a method id, its spelling
in the aspect — `"hem"` for locomotion —, or `{method, args, version}`);
`declared` the asset’s own declaration for the aspect (the field
`Aspect.declared_by` names), placed by [`DECLARED_OUTRANKS_POLICY`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.DECLARED_OUTRANKS_POLICY)
(the request when nothing was requested, or the policy’s last entry);
`policy` the layered shot/style policy; `entity_kind` the asset’s kind,
checked against the aspect’s `applies_to`. Never raises for a method that
does not apply: it falls back and records why.

* **Return type:**
  [`Resolution`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.Resolution)

### an.semantic.matcher.why_not(method, subjects)

What `method` is missing on `subjects`, each with its remedy (empty: it applies).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Missing`](_autosummary/an.semantic.matcher.html.md#an.semantic.matcher.Missing), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]


# _autosummary/an.semantic.prompt.html.md

# an.semantic.prompt

The `an iterate` system prompt, generated from the vocabulary registry.

ADR 0003 decision 6: the prompt is one of the three surfaces generated from the
registry, never written by hand. [`vocabulary_prompt()`](_autosummary/an.semantic.prompt.html.md#an.semantic.prompt.vocabulary_prompt) renders the IR’s
fields and every registered name — action and entity kinds, motion and
expression presets, camera moves, easings, methods by aspect — each with its
one-sentence description and its params. The *protocol* around it (the patch
operations, the path syntax, the editing rules) is `an iterate`’s own and is
passed in as `preamble` and `postamble`.

```pycon
>>> text = vocabulary_prompt()
>>> "push_in" in text and "tween" in text and "linear" in text
True
```

### Functions

| [`iterate_prompt`](_autosummary/an.semantic.prompt.html.md#an.semantic.prompt.iterate_prompt)(\*, preamble, postamble)   | `preamble` + the generated vocabulary + `postamble` (the `an iterate` protocol).   |
|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| [`vocabulary_prompt`](_autosummary/an.semantic.prompt.html.md#an.semantic.prompt.vocabulary_prompt)(\*[, kinds])            | The IR fields and every registered name, as prompt text.                           |

### an.semantic.prompt.iterate_prompt(, preamble, postamble)

`preamble` + the generated vocabulary + `postamble` (the `an iterate` protocol).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.semantic.prompt.vocabulary_prompt(, kinds=(('action', "Action kinds (an action's \`kind\`)"), ('entity', "Entity kinds (an entity's \`kind\`)"), ('motion_preset', 'Motion presets (\`play\` by name; \`args\` are the parameters shown)'), ('expression_preset', "Expression presets (an \`expression\`'s \`preset\`, a dialogue line's \`emotion\`)"), ('camera_move', 'Camera moves (\`camera: {move: …}\`)'), ('easing', "Easings (a tween's \`easing\`)")))

The IR fields and every registered name, as prompt text.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.semantic.registry.html.md

# an.semantic.registry

The one vocabulary registry (ADR 0003 decision 6): entries and aspects, by owner.

Two kinds of source feed it:

- **registered entries** ([`register_entry()`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.register_entry), [`register_aspect()`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.register_aspect)):
  methods, motion and expression presets, camera moves, IR fields — each with
  an owner (the core, or a genre, through [`an.genres.Genre`](_autosummary/an.genres.html.md#an.genres.Genre)’s
  `vocabulary` and `aspects` fields, so a genre’s entries come out with it);
- **views** ([`register_view()`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.register_view)): entries derived on every read from a
  registry that already exists — the action and entity kinds of
  [`an.genres.registry`](_autosummary/an.genres.registry.html.md#module-an.genres.registry), the easings of [`an.timing.easing`](_autosummary/an.timing.easing.html.md#module-an.timing.easing) — so a kind
  > or an easing is defined once, where it lives, and never copied here (design
  > principle 3).

Like [`an.genres.registry`](_autosummary/an.genres.registry.html.md#module-an.genres.registry) this module imports nothing from `an.ir`: a
view imports its source lazily, when it is read.

```pycon
>>> from an.semantic.entries import Entry
>>> e = register_entry(Entry("demo.wiggle", "motion_preset", name="wiggle"), owner="demo")
>>> lookup("motion_preset", "wiggle") is e, entry("demo.wiggle") is e
(True, True)
>>> drop_owner("demo"); lookup("motion_preset", "wiggle") is None
True
```

### Functions

| [`duplicates`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.duplicates)()                             | Spellings defined twice (a view and a registered entry disagreeing on an id count once: the registered one shadows).       |
|-------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| [`aspect`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.aspect)(name)                             | The registered aspect `name`; [`UnknownEntryError`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.UnknownEntryError) otherwise.                |
| `aspect_names`(\*[, owner])                                                               |                                                                                                                            |
| `aspects`()                                                                               |                                                                                                                            |
| [`drop_owner`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.drop_owner)(owner)                        | Remove every entry and aspect `owner` registered.                                                                          |
| [`entries`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.entries)(\*[, kind, owner])               | Every entry (registered and viewed), filtered by `kind`/`owner`, in a stable order.                                        |
| [`entry`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.entry)(entry_id)                          | The entry with this id; [`UnknownEntryError`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.UnknownEntryError) naming the known ids otherwise. |
| [`lookup`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.lookup)(kind, name, \*[, aspect])         | The entry of `kind` a document spells `name` (for methods, within `aspect`).                                               |
| [`methods_of`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.methods_of)(aspect_name)                  | Every registered method of an aspect: its chain first, in order, then the rest.                                            |
| `owner_of`(entry_id)                                                                      |                                                                                                                            |
| [`register_aspect`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.register_aspect)(a, \*[, owner, replace]) | Register an aspect and its default chain.                                                                                  |
| [`register_entry`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.register_entry)(e, \*[, owner, replace])  | Register a vocabulary entry under `owner`: ONE entry per id, and per spelling.                                             |
| [`register_view`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.register_view)(name, view)                | Register a read-time view (core only: the kinds and easing registries).                                                    |
| [`replacements`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.replacements)()                           | Every deliberate replacement: `{id: (previous owner, new owner, previous version)}`.                                       |
| `restore`(state)                                                                          |                                                                                                                            |
| [`snapshot`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.snapshot)()                               | The state of the registered tables (views are code, not state).                                                            |

### Exceptions

| [`UnknownEntryError`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.UnknownEntryError)   | A name or id is not in the vocabulary; the message lists what is.   |
|----------------------------------------------------------------------|---------------------------------------------------------------------|

### *exception* an.semantic.registry.UnknownEntryError

Bases: [`VocabularyError`](_autosummary/an.semantic.html.md#an.semantic.VocabularyError), [`KeyError`](https://docs.python.org/3/builtins/exceptions.html#KeyError)

A name or id is not in the vocabulary; the message lists what is.

### an.semantic.registry.aspect(name)

The registered aspect `name`; [`UnknownEntryError`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.UnknownEntryError) otherwise.

* **Return type:**
  [`Aspect`](_autosummary/an.semantic.html.md#an.semantic.Aspect)

### an.semantic.registry.drop_owner(owner)

Remove every entry and aspect `owner` registered.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.semantic.registry.duplicates()

Spellings defined twice (a view and a registered entry disagreeing on an id
count once: the registered one shadows). Empty when one name is one thing.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.semantic.registry.entries(, kind=None, owner=None)

Every entry (registered and viewed), filtered by `kind`/`owner`, in a stable order.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.semantic.registry.entry(entry_id)

The entry with this id; [`UnknownEntryError`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.UnknownEntryError) naming the known ids otherwise.

* **Return type:**
  [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry)

### an.semantic.registry.lookup(kind, name, , aspect=None)

The entry of `kind` a document spells `name` (for methods, within `aspect`).

* **Return type:**
  [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.semantic.registry.methods_of(aspect_name)

Every registered method of an aspect: its chain first, in order, then the rest.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Method`](_autosummary/an.semantic.html.md#an.semantic.Method), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.semantic.registry.register_aspect(a, , owner='an', replace=False)

Register an aspect and its default chain. Checked as a whole by
[`an.semantic.check_registry()`](_autosummary/an.semantic.html.md#an.semantic.check_registry) (a chain may name methods registered
after it, within the same genre).

* **Return type:**
  [`Aspect`](_autosummary/an.semantic.html.md#an.semantic.Aspect)

### an.semantic.registry.register_entry(e, , owner='an', replace=False)

Register a vocabulary entry under `owner`: ONE entry per id, and per spelling.

A name means one thing everywhere (`push_in` is one entry, an#257): a
second entry under a taken id — registered or provided by a view — or under
a taken `(kind, name)` (within an aspect, for methods) raises, unless
`replace=True` is passed explicitly, and then the replacement is recorded
([`replacements()`](_autosummary/an.semantic.registry.html.md#an.semantic.registry.replacements)). Re-registering the same entry is a no-op.

* **Return type:**
  [`Entry`](_autosummary/an.semantic.html.md#an.semantic.Entry)

```pycon
>>> from an.semantic.entries import Entry
>>> register_entry(Entry("demo.push_in", "camera_move", name="push_in"), owner="demo")
Traceback (most recent call last):
...
an.semantic.entries.VocabularyError: camera_move 'push_in' is already defined by 'camera.push_in' (owner 'an'); ...
```

### an.semantic.registry.register_view(name, view)

Register a read-time view (core only: the kinds and easing registries).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.semantic.registry.replacements()

Every deliberate replacement: `{id: (previous owner, new owner, previous version)}`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### an.semantic.registry.snapshot()

The state of the registered tables (views are code, not state).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)


# _autosummary/an.semantic.seeds.html.md

# an.semantic.seeds

The core’s vocabulary: views over the kind and easing registries, camera moves, IR fields.

ADR 0003’s first slice seeds the registry with today’s named vocabularies.
The **core’s** share is here (a genre contributes the rest through
[`an.genres.Genre`](_autosummary/an.genres.html.md#an.genres.Genre)):

- **action kinds** and **entity kinds** — *views* over
  [`an.genres.registry`](_autosummary/an.genres.registry.html.md#module-an.genres.registry), so a genre’s kinds appear as entries the moment it
  > registers them, with their owner and [`version`](_autosummary/an.genres.html.md#an.genres.ActionKind.version);
- **easings** — a view over [`an.timing.easing.easing_entries()`](_autosummary/an.timing.easing.html.md#an.timing.easing.easing_entries) (the
  easing canon already carries a version per entry);
- **camera moves** — a view over [`an.ir.camera.CAMERA_MOVES`](_autosummary/an.ir.camera.html.md#an.ir.camera.CAMERA_MOVES), versioned
  in [`CAMERA_MOVE_VERSIONS`](_autosummary/an.semantic.seeds.html.md#an.semantic.seeds.CAMERA_MOVE_VERSIONS) (a test fails when a move has none); each is
  a path through the `framing2d` **view space** and requires an engine that
  lowers it (`space.framing2d`, [`an.semantic.views`](_autosummary/an.semantic.views.html.md#module-an.semantic.views), an#257);
- **view spaces** — `framing2d` and `orbit3d` ([`an.semantic.views`](_autosummary/an.semantic.views.html.md#module-an.semantic.views));
- **IR fields** (kind `field`) — what the scene document’s fields accept,
  the notes the `an iterate` prompt used to hand-list. Each says which
  spectrum levels it takes: a field that accepts only (a) says so, and no
  resolver will invent a value for it (ADR 0003 decision 5).

Nothing here imports `an.ir` at module level; the views import their sources
when read.

```pycon
>>> from an.semantic.registry import lookup
>>> lookup("action", "tween").version, lookup("easing", "linear").kind
('1', 'easing')
```

### Module Attributes

| [`CAMERA_MOVE_VERSIONS`](_autosummary/an.semantic.seeds.html.md#an.semantic.seeds.CAMERA_MOVE_VERSIONS)     | Version of each named camera move (ADR 0003).      |
|---------------------------------------------------------------------------|----------------------------------------------------|
| [`CAMERA_MOVE_DESCRIPTIONS`](_autosummary/an.semantic.seeds.html.md#an.semantic.seeds.CAMERA_MOVE_DESCRIPTIONS) | One sentence per camera move, in production terms. |
| [`CORE_FIELDS`](_autosummary/an.semantic.seeds.html.md#an.semantic.seeds.CORE_FIELDS)              | the core).                                         |

### Functions

| [`schema_of_callable`](_autosummary/an.semantic.seeds.html.md#an.semantic.seeds.schema_of_callable)(fn, \*[, skip, positional])   | A JSON Schema object for `fn`'s keyword parameters, defaults included.   |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|

### an.semantic.seeds.CAMERA_MOVE_DESCRIPTIONS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'hold': 'a locked-off camera: no move', 'pan_left': 'truck the camera left across the frame (on a flat stage a pan and a truck look the same)', 'pan_right': 'truck the camera right across the frame', 'pull_out': 'a slow pull out: zoom 1.0 → 0.8 over the shot, eased', 'push_in': 'a slow push in: zoom 1.0 → 1.25 over the shot, eased', 'tilt_down': 'move the camera down across the frame', 'tilt_up': 'move the camera up across the frame (spans the frame height)', 'zoom_in': 'a stronger zoom in: 1.0 → 1.5 over the shot', 'zoom_out': 'a stronger zoom out: 1.0 → 0.7 over the shot'}*

One sentence per camera move, in production terms.

### an.semantic.seeds.CAMERA_MOVE_VERSIONS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'hold': '1', 'pan_left': '1', 'pan_right': '1', 'pull_out': '1', 'push_in': '1', 'tilt_down': '1', 'tilt_up': '1', 'zoom_in': '1', 'zoom_out': '1'}*

Version of each named camera move (ADR 0003). Bump one when its keys change.

### an.semantic.seeds.CORE_FIELDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Entry](_autosummary/an.semantic.html.md#an.semantic.Entry), ...]* *= (Entry(id='field.meta', kind='field', version='1', name='meta', title='', description="the film's header", usage='meta: {title, author, duration, fps, resolution, default_renderer, notes, default_easing, step_hz, style_pack, sounds, captions, closing_transition}', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot', kind='field', version='1', name='shot', title='', description='one shot of the timeline', usage='timeline: a list of shots, each with id (string, unique), renderer ("cutout" | "stage" | "manim" | "motion_graphics" | "whiteboard"), duration (seconds, float), camera, entities, actions, dialogue, narration, transition, sounds', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.camera', kind='field', version='1', name='shot.camera', title='', description="the shot's camera", usage='camera: {move: <a camera move>, ...} or explicit {keys: [...]}', params={}, examples=(), requires=(), levels=frozenset({'a', 'b-name'}), aspects=()), Entry(id='field.shot.camera.follow', kind='field', version='1', name='shot.camera.follow', title='', description='a camera that follows an entity', usage="camera.follow (optional): {target: <entity id or node path>, axes: x|y|xy} — the camera tracks the target, keeping it where it stood at the shot's start (a walk across a long set); planes parallax with it. Instead of a move or keys (hold is fine); a shake layers on top.", params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.camera.shake', kind='field', version='1', name='shot.camera.shake', title='', description='a jolt of the frame inside the shot', usage='camera.shake (optional): jolts of the frame layered on the move or keys — [{at: s, duration: 0.4, amplitude: 0.015, frequency: 24, decay: true, seed: 0}], amplitude a fraction of the frame height; for an impact, a slam, an explosion. The frame is at rest before \`at\` and after the end; shakes may not overlap or run past the shot.', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.entities', kind='field', version='1', name='shot.entities', title='', description='who and what is on stage', usage='entities: list of {kind, id, store, ref, ...}; kind MUST be a registered entity kind. A prop needs a PropDescriptor in the props store; it has no placeholder rig, so an unknown ref raises rather than drawing a person.', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.actions', kind='field', version='1', name='shot.actions', title='', description="the shot's animation", usage='actions: list of action dicts whose kind is a registered action kind (the composites sequence, parallel, delay and loop hold children).', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.actions.property', kind='field', version='1', name='shot.actions.property', title='', description='what a set or tween animates', usage="A tween/set action's property is EITHER a transform: alpha, dash_offset, perspective, pivot_x, pivot_y, plane_fade_end, plane_fade_start, rotation, rotation_rad, rotation_x, scale_x, scale_y, skew_x, skew_y, trim_end, trim_start, x, y — OR 'tint', a per-node colour MULTIPLY whose value is a '#rrggbb' string (the compiler expands it into three numeric channels, so a tween between two colours interpolates per channel; like 'alpha' it cascades to the target's parts). 'alpha' is the fade primitive and cascades to a character's parts. On a text block, 'text' swaps a replacement set's string ('texts', unit 'block'; the value is a key) and 'value' is a counter block's number ('counter'; set or tween a number, lowered at compile to the strings the frames show). Any other property (opacity, visible, color, width, ...) is refused at compile. A tween with no 'from' starts at the property's rest value: 1.0 for scale_x / scale_y / alpha / trim_end / perspective, '#ffffff' for tint, 0.0 for the rest. rotation_x / perspective / plane_fade_start / plane_fade_end tilt the node's plane away from the camera (radians, frame heights, plane px), and then pivot_y slides its content along the plane. A tween with no 'easing' takes the scene's meta.default_easing when set.", params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.actions.easing', kind='field', version='1', name='shot.actions.easing', title='', description='how a tween moves through time', usage="A tween's easing is a registered easing name, a cubic-Bézier 4-list [cx1, cy1, cx2, cy2], or a parametrised curve such as 'cubic-bezier(…)' or 'steps(n)'.", params={}, examples=(), requires=(), levels=frozenset({'a', 'b-name'}), aspects=()), Entry(id='field.shot.dialogue', kind='field', version='1', name='shot.dialogue', title='', description='who says what, and when', usage="dialogue: list of {speaker, text, emotion, voice_ref, pause, at, direction, ...}. Lines play back to back from the shot start. 'pause' (seconds) is silence before a line, after the previous one ends — a beat, a look, a hesitation belongs here, NOT in a new shot. 'at' (seconds) starts a line at that shot time instead; a line takes one or the other, never both (to switch, delete the one you are replacing in the same patch list). 'start' and 'duration' are stamped by the audio pipeline from these on every render — never patch them.", params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.dialogue.direction', kind='field', version='1', name='shot.dialogue.direction', title='', description='how a line is delivered', usage="direction (optional) is a list of delivery cues — ['excited'], ['sighs', 'annoyed'] — that an expressive TTS voice performs; it is never spoken as text and never shown in captions.", params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.narration', kind='field', version='1', name='shot.narration', title='', description="a narrator's lines (not implemented)", usage='narration: list (same shape as dialogue, no speaker pin). NOT IMPLEMENTED — the audio pipeline walks dialogue only, and a shot with narration RAISES. To add a narrator, emit a dialogue line whose speaker is not an entity in the shot; it gets audio and no lip-sync.', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.transition', kind='field', version='1', name='shot.transition', title='', description='how a shot is entered', usage='transition (optional): how the shot is ENTERED — {kind: "cut" | "fade" | "dissolve" | "wipe", duration: seconds, color: \\'#rrggbb\\'}. Omitted = a hard cut. \\'fade\\' dips through color (half out of the previous shot, half into this one; on the first shot, a fade up). \\'dissolve\\' overlaps the two shots by duration, so the film gets that much shorter; never on the first shot. \\'wipe\\' overlaps the same way, but a hard edge sweeps across the frame in direction (\\'left\\', \\'right\\', \\'up\\' or \\'down\\': the way the edge travels). A shot must be long enough to hold its own transition and the next shot\\'s.', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.shot.sounds', kind='field', version='1', name='shot.sounds', title='', description="sound effects on the shot's clock", usage='sounds (optional): SFX cues in SHOT-local time — [{sound: <key in the sounds store>, at, [duration], [gain_db], [loop], [fade_in], [fade_out], [duck_db]}]. Never invent a sound key.', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.meta.sounds', kind='field', version='1', name='meta.sounds', title='', description="sounds on the film's clock (a music bed)", usage='meta.sounds (optional): the same cue shape in FILM time — a music bed is {sound: <key>, loop: true, duck_db: -12, fade_in, fade_out}; duck_db ducks it under every dialogue line.', params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.meta.closing_transition', kind='field', version='1', name='meta.closing_transition', title='', description='how the film ends', usage="meta.closing_transition (optional): how the film ENDS — {kind: 'fade', duration: seconds, color: '#rrggbb'}: the last shot's last duration fades to color (its final frame IS the color) and the sound fades with it; the film's length is unchanged. Omitted (or 'cut') = it ends on its last frame. A dissolve is refused.", params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), Entry(id='field.meta.captions', kind='field', version='1', name='meta.captions', title='', description='captions derived from the dialogue', usage="meta.captions (optional): captions built at render time from the dialogue's word timings — {} for the defaults, or {highlight: '#rrggbb', color, size, anchor, max_chars, max_lines, burn, sidecar, strict}. Never add caption text entities by hand: they are derived from the dialogue.", params={}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()))*

the core).

* **Type:**
  The core’s IR-field entries, registered on import (owner

### an.semantic.seeds.schema_of_callable(fn, , skip=(), positional=False)

A JSON Schema object for `fn`’s keyword parameters, defaults included.

Positional-only and `skip``ped parameters are left out (``target`,
`rest`, `parts` are the compiler’s, not the author’s).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> def f(target, *, height: float = 30.0, label: str = "x", n: int | None = None): ...
>>> schema_of_callable(f, skip=("target",))["properties"]["height"]
{'type': 'number', 'default': 30.0}
```


# _autosummary/an.semantic.views.html.md

# an.semantic.views

View spaces: what a camera move moves through, defined once for every engine (an#257).

A camera move (`push_in`, `pan_left`, an orbit) is not a crop, a zoom or a
parameter tween: it is a **path through a view space**, which each engine
lowers its own way —

- a **crop engine** (`burns`, a still or a video) moves a rectangle over
  fixed pixels, so pushing in loses resolution;
- a **render engine** (the stage engine of `an`/`cutan`, vector art)
  re-renders the scene into the rectangle, so pushing in loses nothing;
- a **parameter engine** (`previz`) moves a vector of view parameters — an
  orbit camera in 3D, a chart’s axis domain.

So the vocabulary defines each move once, over an abstract space, and an engine
affords the space it can lower (`space.framing2d` is an engine capability,
[`an.capabilities.subjects`](_autosummary/an.capabilities.subjects.html.md#module-an.capabilities.subjects)). The core declares two spaces; a genre or a
package declares more (an n-dimensional one) with [`view_space()`](_autosummary/an.semantic.views.html.md#an.semantic.views.view_space), and
registers moves over them into the same `camera_move` table — so `push_in`
means one thing everywhere, and `burns`’ moves and `previz`’s camera
formulas join it instead of redefining it.

```pycon
>>> FRAMING_2D.entry.id, FRAMING_2D.capability.name, FRAMING_2D.entry.params["properties"]["zoom"]["scale"]
('view.framing2d', 'space.framing2d', 'log')
```

### Module Attributes

| [`FRAMING_2D`](_autosummary/an.semantic.views.html.md#an.semantic.views.FRAMING_2D)   | where the frame is, how close, how rolled.                 |
|---------------------------------------------------------------|------------------------------------------------------------|
| [`ORBIT_3D`](_autosummary/an.semantic.views.html.md#an.semantic.views.ORBIT_3D)     | a camera on a sphere around a target (previz's turntable). |

### Functions

| [`view_space`](_autosummary/an.semantic.views.html.md#an.semantic.views.view_space)(name, fields, \*, description[, ...])   | Declare a view space: an entry `view.<name>` and an engine capability `space.<name>`.   |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|

### Classes

| [`ViewField`](_autosummary/an.semantic.views.html.md#an.semantic.views.ViewField)(name, unit[, scale, rest, description])   | One axis of a view space: its name, unit, how it interpolates, its rest value.     |
|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| [`ViewSpace`](_autosummary/an.semantic.views.html.md#an.semantic.views.ViewSpace)(entry, capability)                        | A declared view space: its vocabulary entry and the engine capability to lower it. |

### an.semantic.views.FRAMING_2D *: [ViewSpace](_autosummary/an.semantic.views.html.md#an.semantic.views.ViewSpace)* *= ViewSpace(entry=Entry(id='view.framing2d', kind='view_space', version='1', name='framing2d', title='', description='a 2D framing of a flat picture: position, zoom (log), roll (angle)', usage='axes: x (frame widths, linear), y (frame heights, linear), zoom (ratio, log), rotation (rad, angle)', params={'type': 'object', 'properties': {'x': {'type': 'number', 'unit': 'frame widths', 'scale': 'linear', 'default': 0.0, 'description': '+x moves the view right'}, 'y': {'type': 'number', 'unit': 'frame heights', 'scale': 'linear', 'default': 0.0, 'description': '+y moves the view down'}, 'zoom': {'type': 'number', 'unit': 'ratio', 'scale': 'log', 'default': 1.0, 'description': 'on-screen magnification; > 1 is closer'}, 'rotation': {'type': 'number', 'unit': 'rad', 'scale': 'angle', 'default': 0.0, 'description': "the view's roll"}}}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), capability=Capability(name='space.framing2d', description='the engine lowers moves through the framing2d view space: a 2D framing of a flat picture: position, zoom (log), roll (angle)', remedy='render with an engine that lowers the framing2d view space', subject='engine', command=None, version='1'))*

where the frame is, how close, how rolled. Every
engine that shows a flat picture lowers it — by cropping pixels (burns) or by
re-rendering into the frame (the stage engine).

* **Type:**
  The 2D framing space

### an.semantic.views.ORBIT_3D *: [ViewSpace](_autosummary/an.semantic.views.html.md#an.semantic.views.ViewSpace)* *= ViewSpace(entry=Entry(id='view.orbit3d', kind='view_space', version='1', name='orbit3d', title='', description='an orbit camera around a 3D target: azimuth and elevation (angles), distance (log)', usage='axes: azimuth (rad, angle), elevation (rad, angle), distance (scene units, log), target_x (scene units, linear), target_y (scene units, linear), target_z (scene units, linear)', params={'type': 'object', 'properties': {'azimuth': {'type': 'number', 'unit': 'rad', 'scale': 'angle', 'default': 0.0, 'description': 'around the target'}, 'elevation': {'type': 'number', 'unit': 'rad', 'scale': 'angle', 'default': 0.0, 'description': "above the target's horizon"}, 'distance': {'type': 'number', 'unit': 'scene units', 'scale': 'log', 'default': 1.0, 'description': 'from the target'}, 'target_x': {'type': 'number', 'unit': 'scene units', 'scale': 'linear', 'default': 0.0}, 'target_y': {'type': 'number', 'unit': 'scene units', 'scale': 'linear', 'default': 0.0}, 'target_z': {'type': 'number', 'unit': 'scene units', 'scale': 'linear', 'default': 0.0}}}, examples=(), requires=(), levels=frozenset({'a'}), aspects=()), capability=Capability(name='space.orbit3d', description='the engine lowers moves through the orbit3d view space: an orbit camera around a 3D target: azimuth and elevation (angles), distance (log)', remedy='render with an engine that lowers the orbit3d view space', subject='engine', command=None, version='1'))*

a camera on a sphere around a target (previz’s turntable).

* **Type:**
  The 3D orbit space

### *class* an.semantic.views.ViewField(name, unit, scale='linear', rest=0.0, description='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One axis of a view space: its name, unit, how it interpolates, its rest value.

### *class* an.semantic.views.ViewSpace(entry, capability)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A declared view space: its vocabulary entry and the engine capability to lower it.

### an.semantic.views.view_space(name, fields, , description, version='1')

Declare a view space: an entry `view.<name>` and an engine capability `space.<name>`.

Register both through a [`an.genres.Genre`](_autosummary/an.genres.html.md#an.genres.Genre) (`vocabulary` and
`capabilities`) or, for the core’s, directly.

* **Return type:**
  [`ViewSpace`](_autosummary/an.semantic.views.html.md#an.semantic.views.ViewSpace)


# _autosummary/an.sound_anchors.html.md

# an.sound_anchors

Sound cues timed by the picture (an#317): `at: {when: <node>, reaches: {y: px}}` and `until:`.

A record scratch has to land when the last line of a crawl becomes readable.
Writing that second into the scene means solving the crawl’s geometry outside
`an`, and every layout change moves the picture away from the sound. Here the
cue names the event instead, and [`resolve_cue_anchors()`](_autosummary/an.sound_anchors.html.md#an.sound_anchors.resolve_cue_anchors) finds the second
when the film is laid out:

- `at: {when: crawl/line_7, reaches: {y: 840}}` — the first instant the
  node’s on-screen centre crosses that frame row (or `x` column), from the
  very document the stage renders (`compiled_document`), through the camera,
  parallax and a crawl’s tilt ([`an.stage.timeline.screen_position()`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.screen_position)),
  sampled at every film frame and interpolated between the two that straddle
  it; plus `offset`;
- `until: {cue: scratch, offset: 0.12}` — the cue ends at that cue’s start
  plus `offset` (its `duration` is derived).

It runs on the in-memory settled copy of the scene (as the measured shot
lengths do, [`an.measurements`](_autosummary/an.measurements.html.md#module-an.measurements)): `scene.md` keeps the anchor, never the
second. Each resolved cue is reported as an `info` finding — “scratch at
33.03 s (crawl/line_7 reached y=840)” — which the render report records. An
anchor that cannot be resolved (no such node, never reached, a shot no stage
draws) refuses before any browser launches.

### Functions

| [`resolve_cue_anchors`](_autosummary/an.sound_anchors.html.md#an.sound_anchors.resolve_cue_anchors)(scene, ctx, renderers, ...)   | `scene` (a copy) with every anchored `at` resolved to seconds and every `until` to a duration.   |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|

### Exceptions

| [`SoundAnchorError`](_autosummary/an.sound_anchors.html.md#an.sound_anchors.SoundAnchorError)   | A cue's anchor cannot be resolved: the message says which and why.   |
|---------------------------------------------------------------------|----------------------------------------------------------------------|

### *exception* an.sound_anchors.SoundAnchorError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A cue’s anchor cannot be resolved: the message says which and why.

### an.sound_anchors.resolve_cue_anchors(scene, ctx, renderers, , film_starts)

`scene` (a copy) with every anchored `at` resolved to seconds and every `until` to a duration.

ctx: the render context the shots are compiled under
renderers: each shot’s renderer, in timeline order
film_starts: each shot’s start in film time (a `meta.sounds` anchor’s

> shot-local second is moved there)
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`SceneIR`](_autosummary/an.ir.schema.html.md#an.ir.schema.SceneIR), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/an.verify.html.md#an.verify.Finding)]]


# _autosummary/an.sound_fetch.html.md

# an.sound_fetch

Add a sound from a URL, with its provenance and the cut taken (an#318).

A production that sources its music or an effect from the web used to write
the same script each time: download, cut with ffmpeg, type an `AssetSource`
by hand, [`an.sounds.add_sound()`](_autosummary/an.sounds.html.md#an.sounds.add_sound). [`add_sound_from_url()`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.add_sound_from_url) is that script:

- the download is a seam, `fetcher=`: any `(url) -> Fetched` (the bytes in
  any container ffmpeg reads, and what the source says about itself). The
  default, [`yb_fetcher()`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.yb_fetcher), uses [yb](https://github.com/thorwhalen/yb)
  when it is installed (`pip install yb` with its `yt-dlp` extra); it is
  never a dependency of `an`. Another ingest (braidio’s) plugs in here;
- the cut (`start`, `duration`, `fade_out`) is made by ffmpeg into a
  16-bit WAV, and recorded with the source, so `an credits` says which part
  of which page it is (“0:07.9–0:08.8 of <url>”);
- the licence is the caller’s statement, never guessed: required, and
  `"unknown"` says so explicitly (the sound is then UNVERIFIED).

```pycon
>>> fetched = Fetched(audio=b"...", url="https://example.org/a", id="a", author="Ann")
>>> fetched.provider
'web'
```

### Module Attributes

| [`Fetcher`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.Fetcher)   | how [`add_sound_from_url()`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.add_sound_from_url) gets the media.   |
|------------------------------------------------------------|---------------------------------------------------------------------------------------------|

### Functions

| [`add_sound_from_url`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.add_sound_from_url)(store, key, url, \*, license)   | Fetch `url`, cut it, and put it in the sounds `store` under `key` with its provenance.   |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [`cut_to_wav`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.cut_to_wav)(audio, \*[, start, duration, fade_out]) | `audio` (any container ffmpeg reads) cut to `[start, start + duration]`, as 16-bit WAV.  |
| [`yb_fetcher`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.yb_fetcher)(url)                                    | Fetch the audio of `url` with `yb` (any site its `yt-dlp` reads).                        |

### Classes

| [`Fetched`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.Fetched)(audio, url[, id, title, author, ...])   | What a fetcher returns: the media's bytes, and what its page says about it.   |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|

### *class* an.sound_fetch.Fetched(audio, url, id=None, title=None, author=None, provider='web')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a fetcher returns: the media’s bytes, and what its page says about it.

#### provider *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'web'*

Where it came from, as an `AssetSource.provider` (`youtube`, …).

### an.sound_fetch.Fetcher

how [`add_sound_from_url()`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.add_sound_from_url) gets the media.

* **Type:**
  `(url) -> Fetched`

alias of [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Fetched`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.Fetched)]

### an.sound_fetch.add_sound_from_url(store, key, url, , license, fetcher=None, start=None, duration=None, fade_out=None, provider=None, author=None, description='')

Fetch `url`, cut it, and put it in the sounds `store` under `key` with its provenance.

* **Return type:**
  [`SoundAsset`](_autosummary/an.sounds.html.md#an.sounds.SoundAsset)

license: the licence code the caller vouches for — required, never
: guessed; `"unknown"` (or `None`) records that nobody has said

fetcher: `(url) -> Fetched` (default [`yb_fetcher()`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.yb_fetcher))
start, duration, fade_out: the cut, in seconds of the fetched media
provider, author: override what the fetcher reported

### an.sound_fetch.cut_to_wav(audio, , start=None, duration=None, fade_out=None)

`audio` (any container ffmpeg reads) cut to `[start, start + duration]`, as 16-bit WAV.

`fade_out`: seconds of linear fade at the end of the cut.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### an.sound_fetch.yb_fetcher(url)

Fetch the audio of `url` with `yb` (any site its `yt-dlp` reads).

* **Return type:**
  [`Fetched`](_autosummary/an.sound_fetch.html.md#an.sound_fetch.Fetched)


# _autosummary/an.sounds.html.md

# an.sounds

Sound assets: what the sound layer plays, where it came from, and a synthesizer.

A [`an.ir.schema.SoundCue`](_autosummary/an.ir.schema.html.md#an.ir.schema.SoundCue) names a key in the project’s `sounds` store;
this module is the typed front door to that store. Every sound carries an
[`AssetSource`](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) — provenance and licence — and the sha256 of
the bytes that licence is attached to, because a sound is third-party work far
more often than a character is, and \*\*a licence recorded and never displayed is
not compliance\*\*: `an credits` walks this store like the others.

```pycon
>>> import tempfile
>>> from an.stores.sounds import SoundsStore
>>> with tempfile.TemporaryDirectory() as d:
...     store = SoundsStore(d)
...     asset = add_sound(store, "beep", synth_tone(440.0, 0.25), source=SYNTH_SOURCE)
...     asset.duration, get_sound(store, "beep")[1][:4]
(0.25, b'RIFF')
```

**The film mix is mono at 44.1 kHz** (the per-shot audio’s format), so a
stereo asset is mixed down. **v1 stores WAV (PCM) only.** The mix needs each asset’s exact length to place
a fade-out, and a WAV header states it without decoding anything; convert other
formats with ffmpeg before adding them.

The synthesizers ([`synth_tone()`](_autosummary/an.sounds.html.md#an.sounds.synth_tone), [`synth_hit()`](_autosummary/an.sounds.html.md#an.sounds.synth_hit), [`synth_bed()`](_autosummary/an.sounds.html.md#an.sounds.synth_bed)) are
what the demo and the tests use instead of shipping any third-party audio: pure
numpy, seeded, so the same call writes the same bytes.

### Module Attributes

| [`SYNTH_SOURCE`](_autosummary/an.sounds.html.md#an.sounds.SYNTH_SOURCE)   | The provenance of everything [`synth_tone()`](_autosummary/an.sounds.html.md#an.sounds.synth_tone) / [`synth_hit()`](_autosummary/an.sounds.html.md#an.sounds.synth_hit) / [`synth_bed()`](_autosummary/an.sounds.html.md#an.sounds.synth_bed) produce: generated on the user's machine by `an` from numbers, so no third party's work is in it.   |
|-----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### Functions

| [`add_sound`](_autosummary/an.sounds.html.md#an.sounds.add_sound)(store, key, audio, \*, source[, ...])    | Put `audio` (WAV bytes) in `store` under `key`, with its provenance.                                                                                                                                                                |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`get_sound`](_autosummary/an.sounds.html.md#an.sounds.get_sound)(store, key)                              | `(asset, wav_bytes)` for `key`, the bytes checked against the digest.                                                                                                                                                               |
| [`synth_bed`](_autosummary/an.sounds.html.md#an.sounds.synth_bed)(duration, \*[, chord, pulse_hz, ...])    | A music bed: a sustained chord with a gentle pulse, loopable end to end.                                                                                                                                                            |
| [`synth_hit`](_autosummary/an.sounds.html.md#an.sounds.synth_hit)([duration, seed, thump_hz, decay, ...])  | A percussive hit: a seeded noise burst over a low thump, decaying fast.                                                                                                                                                             |
| [`synth_tone`](_autosummary/an.sounds.html.md#an.sounds.synth_tone)(freq, duration, \*[, sample_rate, ...]) | A sine at `freq` Hz, with short linear ramps so it does not click.                                                                                                                                                                  |
| [`wav_duration`](_autosummary/an.sounds.html.md#an.sounds.wav_duration)(data)                                 | Seconds of audio in a PCM WAV ([`wav_info()`](_autosummary/an.sounds.html.md#an.sounds.wav_info): the bytes, not the header).                                                                                                             |
| [`wav_info`](_autosummary/an.sounds.html.md#an.sounds.wav_info)(data)                                     | `(sample_rate, channels, frames)` of a PCM WAV: its format from the header, its length from the audio bytes actually present (an#330).                                                                                              |
| [`well_formed_wav`](_autosummary/an.sounds.html.md#an.sounds.well_formed_wav)(data)                              | `data` with a data size that states its true length — the SAME bytes whenever it already does (a trailing `LIST` chunk, a pad byte and the format are kept, so the digest of a healthy file is the digest of the file as supplied). |

### Classes

| [`SoundAsset`](_autosummary/an.sounds.html.md#an.sounds.SoundAsset)(\*\*data)   | The `sound.json` of one entry in the `sounds` store.   |
|-------------------------------------------------------------------------|--------------------------------------------------------|

### Exceptions

| [`SoundError`](_autosummary/an.sounds.html.md#an.sounds.SoundError)   | A sound the store cannot hold, or holds wrongly.   |
|---------------------------------------------------------------|----------------------------------------------------|

### an.sounds.SYNTH_SOURCE *= AssetSource(provider='an.sounds', id='procedural-synthesis', url=None, license='cc0-1.0', license_url=None, attribution=None, source_page_url=None, author='an (procedural synthesis, generated locally)', author_url=None, cacheable=None, sha256=None, cost_usd=None, extra={})*

The provenance of everything [`synth_tone()`](_autosummary/an.sounds.html.md#an.sounds.synth_tone) / [`synth_hit()`](_autosummary/an.sounds.html.md#an.sounds.synth_hit) /
[`synth_bed()`](_autosummary/an.sounds.html.md#an.sounds.synth_bed) produce: generated on the user’s machine by `an` from
numbers, so no third party’s work is in it.

### *class* an.sounds.SoundAsset(\*\*data)

Bases: `BaseModel`

The `sound.json` of one entry in the `sounds` store.

#### duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Seconds of audio, from the audio bytes ([`wav_duration()`](_autosummary/an.sounds.html.md#an.sounds.wav_duration); before
an#330 a pipe-cut WAV recorded its streaming header’s ~22369 s).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### sha256 *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Digest of `audio.wav` as it entered the project; checked on every read.

### *exception* an.sounds.SoundError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A sound the store cannot hold, or holds wrongly.

### an.sounds.add_sound(store, key, audio, , source, description='')

Put `audio` (WAV bytes) in `store` under `key`, with its provenance.

`source` is required: a sound with no recorded origin is exactly the
asset `an credits` cannot vouch for. Its `license` may be `None` —
that is recorded as UNKNOWN and reported as unverified, never as free. A
source whose terms forbid keeping the bytes (`cacheable=False`: a
Freesound API pull) is refused: the store keeps bytes (an#332).

* **Return type:**
  [`SoundAsset`](_autosummary/an.sounds.html.md#an.sounds.SoundAsset)

### an.sounds.get_sound(store, key)

`(asset, wav_bytes)` for `key`, the bytes checked against the digest.

A mismatch raises: the licence is attached to the digest, so different
bytes under the same key are an asset nobody recorded.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`SoundAsset`](_autosummary/an.sounds.html.md#an.sounds.SoundAsset), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

### an.sounds.synth_bed(duration, , chord=(220.0, 277.18, 329.63), pulse_hz=2.0, sample_rate=44100, amplitude=0.3)

A music bed: a sustained chord with a gentle pulse, loopable end to end.

The pulse is a whole number of cycles over `duration` when
`duration * pulse_hz` is whole, so a looped bed does not bump at the seam.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> len(synth_bed(1.0)) > 44 and synth_bed(1.0) == synth_bed(1.0)
True
```

### an.sounds.synth_hit(duration=0.35, , seed=0, thump_hz=90.0, decay=0.06, sample_rate=44100, amplitude=0.6)

A percussive hit: a seeded noise burst over a low thump, decaying fast.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> synth_hit(seed=1) == synth_hit(seed=1), synth_hit(seed=1) == synth_hit(seed=2)
(True, False)
```

### an.sounds.synth_tone(freq, duration, , sample_rate=44100, amplitude=0.3, attack=0.005, release=0.02)

A sine at `freq` Hz, with short linear ramps so it does not click.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> synth_tone(440.0, 0.1) == synth_tone(440.0, 0.1)
True
```

### an.sounds.wav_duration(data)

Seconds of audio in a PCM WAV ([`wav_info()`](_autosummary/an.sounds.html.md#an.sounds.wav_info): the bytes, not the header).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> wav_duration(synth_tone(440.0, 0.5, sample_rate=8000))
0.5
```

### an.sounds.wav_info(data)

`(sample_rate, channels, frames)` of a PCM WAV: its format from the
header, its length from the audio bytes actually present (an#330).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> wav_info(synth_tone(440.0, 0.5, sample_rate=8000))
(8000, 1, 4000)
```

### an.sounds.well_formed_wav(data)

`data` with a data size that states its true length — the SAME bytes
whenever it already does (a trailing `LIST` chunk, a pad byte and the
format are kept, so the digest of a healthy file is the digest of the file
as supplied). What [`add_sound()`](_autosummary/an.sounds.html.md#an.sounds.add_sound) stores, so a WAV cut to a pipe is kept
as a file every reader agrees on: only its two size fields are patched and
anything past its audio dropped, the format chunk untouched
(WAVE_FORMAT_EXTENSIBLE and its channel mask included).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> wav = synth_tone(440.0, 0.1, sample_rate=8000)
>>> well_formed_wav(wav) is wav
True
```


# _autosummary/an.sounds_cli.html.md

# an.sounds_cli

`an sounds …`: the project’s sounds store from the shell (an#318).

Thin projections of [`an.sounds`](_autosummary/an.sounds.html.md#module-an.sounds) and [`an.sound_fetch`](_autosummary/an.sound_fetch.html.md#module-an.sound_fetch), wired into the
CLI by `an.tools._dispatch_namespaces`.

### Functions

| [`add`](_autosummary/an.sounds_cli.html.md#an.sounds_cli.add)(project_dir, key, url[, license, start, ...])   | Add a sound to the project from a URL (a YouTube page, any page yb reads), with its provenance.   |
|------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|

### an.sounds_cli.add(project_dir, key, url, license='', start=0.0, duration=0.0, fade_out=0.0, provider='', author='', description='')

Add a sound to the project from a URL (a YouTube page, any page yb reads), with its provenance.

The audio is fetched with yb (pip install yb yt-dlp), cut with ffmpeg and
stored as WAV; the source records the page, its id, title and channel, the
cut, and the licence you give — `an credits` lists it as “0:07.9–0:08.8 of <url>”.

project_dir: the an project
key: the sound’s key in the project’s sounds store (what a cue names)
url: the page the sound is on
license: the licence code you vouch for (required; “unknown” records that nobody has said, and the sound is UNVERIFIED)
start: seconds into the media where the cut begins
duration: seconds kept (default: to the end)
fade_out: seconds of fade at the end of the cut
provider: override the provider the page reports (default: youtube, or the host)
author: override the author the page reports (default: its channel)
description: what the sound is, for the store

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.stage.cache_key.html.md

# an.stage.cache_key

What a cut-out shot render reads: the keyer behind its shot-cache key (ADR 0004).

The cut-out render is a pure function of these, and of nothing else:

- **the compiled document** — `compile_shot()` with exactly the arguments
  the stage engine passes (`StageEngine.open`, since an#247); digested as `scene_contract_sha256`, so the
  cache key and the bench’s contract hash agree about what “the same document”
  means while staying two different things (the key covers more);
- **the bytes of every texture it stages** — SVG included. Raster art already
  carries its digest in the document (an#211); SVG art is addressed by path,
  so an SVG edited in place would otherwise leave the key unchanged. The
  digests go into the KEY only: the wire shape and `scene_contract_sha256`
  do not move (ADR 0004 decision 2);
- **the version of every easing the document names** (an#239 item 1): a
  compiled keyframe carries a bare name, so a v2 of a curve would change
  pixels under an unchanged document;
- **the dialogue audio it muxes** — each line’s `audio_ref`,
  `viseme_ref`, start and the digest of the bytes the store returns for it,
  and the picture’s length the mux cuts to;
- **the JS runtime** (`runtime_sha256`) and **every render knob**, each
  RESOLVED the way the render resolves it (`pix_fmt=None` is the module
  default at call time, which is what the bench’s lever rebinds), plus the
  pinned Chromium and x264 argv.
- **the render path’s Python source** ([`render_code_digest()`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_code_digest)): the
  capture loop, the canvas readback, the supersample and shutter resolves, the
  audio mux — everything that turns the document into an mp4 after compile.
  The Python twin of `runtime_sha256`: an `an` upgrade that changes how a
  shot is encoded (an#195 did, with no knob and no runtime change) re-renders
  every shot instead of serving an old mp4. Computed by walking the imports
  from the REGISTERED renderer’s own modules ([`render_path_roots()`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_path_roots): the
  renderer class, the frame stage its `render` comes from, its engine), and
  through the string targets of `an._shims.forward_module_attributes()`,
  so a new helper module – or a module that became a pure re-export shim –
  cannot fall outside it.

The machine — Chromium build, Playwright, the full ffmpeg build and the x264
build it encodes with, ISA — is the separate environment part
([`cutout_environment()`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.cutout_environment)), never mixed into the content. Fonts: a text
unit’s glyphs are outlined in Python and travel INSIDE the document (`data:`
srcs), so a different face is a different compiled digest; but SVG ART may
carry its own `<text>`, which Chromium draws with the machine’s fonts — so a
shot that stages such a part gets a `fonts` part, a digest of the installed
font set ([`system_fonts_digest()`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.system_fonts_digest)).

### Module Attributes

| [`EASING_KEYS`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.EASING_KEYS)          | Keys in the compiled document whose string value names an easing.                                                                       |
|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
| [`RENDER_PATH_ROOT`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.RENDER_PATH_ROOT)     | The cut-out renderer's historical module -- since an#247 a live alias of `an.stage.render`, followed through its `alias_module` target. |
| [`RENDER_PATH_EXCLUDED`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.RENDER_PATH_EXCLUDED) | Modules the walk does NOT enter, each with the reason its change is already in the key some other way.                                  |
| [`SVG_TEXT_MARKERS`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.SVG_TEXT_MARKERS)     | A byte sequence that marks an SVG part drawing text with the MACHINE's fonts.                                                           |
| [`FORWARDING_CALLS`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.FORWARDING_CALLS)     | The calls that make an old module's names LIVE aliases of another module's (`an._shims`).                                               |
| [`FONT_DIRS`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.FONT_DIRS)            | Font folders listed when `fc-list` is absent.                                                                                           |

### Functions

| [`compiled_document`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.compiled_document)(shot, ctx)                 | The document the stage engine (`StageEngine.open`) will compile for `shot` under `ctx`.                                                                                                                |
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`cutout_environment`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.cutout_environment)()                         | The cut-out render's machine: the bench's own probes, minus what is not identity.                                                                                                                      |
| [`cutout_shot_inputs`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.cutout_shot_inputs)(shot, ctx)                | The cut-out renderer's [`ShotKeyer`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyer).                                                                                             |
| [`easing_versions`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.easing_versions)(doc)                         | `{name: version}` for every registered easing the compiled document names.                                                                                                                             |
| [`ffmpeg_build`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.ffmpeg_build)()                               | The whole `ffmpeg -version` (every library's version and the configure line), not its first line: the banner is unchanged by `brew upgrade x264`, which swaps the dynamically linked encoder under it. |
| [`genre_code_modules`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.genre_code_modules)()                         | `{module: source path}` for every module of every package a loaded genre runs code from.                                                                                                               |
| [`muxed_audio`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.muxed_audio)(shot, ctx)                       | What `_mux_shot` lays under the picture, as data: one entry per muxed line.                                                                                                                            |
| [`render_code_digest`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_code_digest)()                         | sha256 over the source of every module on the render path (by module name).                                                                                                                            |
| [`render_code_modules`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_code_modules)()                        | `{module name: sha256 of its source}` for every module on the render path.                                                                                                                             |
| [`render_knobs`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_knobs)(shot, ctx)                      | Every `RenderContext` knob, resolved the way the frame stage resolves it.                                                                                                                              |
| [`render_path_modules`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_path_modules)([root, excluded, moves]) | `{module: source path}` for every module the render path reaches.                                                                                                                                      |
| [`render_path_roots`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_path_roots)([renderer_type])           | The modules a renderer's render path starts from, read off the renderer itself.                                                                                                                        |
| [`system_fonts_digest`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.system_fonts_digest)()                        | A digest of the fonts this machine can draw SVG `<text>` with; once per process.                                                                                                                       |
| [`texture_digests`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.texture_digests)(scene_json, mall)            | `{alias: sha256 of the bytes staged for it}` for every texture the document declares.                                                                                                                  |
| [`x264_build`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.x264_build)()                                 | The x264 build that ACTUALLY encodes: one 16x16 frame, its SEI read back.                                                                                                                              |

### an.stage.cache_key.EASING_KEYS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'easing'})*

Keys in the compiled document whose string value names an easing.

### an.stage.cache_key.FONT_DIRS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]]* *= {'Darwin': ('/System/Library/Fonts', '/Library/Fonts', '~/Library/Fonts'), 'Linux': ('/usr/share/fonts', '/usr/local/share/fonts', '~/.local/share/fonts', '~/.fonts'), 'Windows': ('C:/Windows/Fonts',)}*

Font folders listed when `fc-list` is absent.

### an.stage.cache_key.FORWARDING_CALLS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'alias_module', 'forward_module_attributes', 'moved_to_package'})*

The calls that make an old module’s names LIVE aliases of another module’s
(`an._shims`). Their target is a STRING, invisible to an import walk.

### an.stage.cache_key.RENDER_PATH_EXCLUDED *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'an.adapters._base': 'the RenderContext/RenderResult types; their values are \`knobs\`', 'an.ir.schema': 'the IR model; what it means for a render reaches \`compiled\`/\`knobs\`', 'an.stage.compile': 'its output is the \`compiled\` part', 'an.stage.serialize': 'its output is the \`compiled\` part', 'an.stage.text_layout': 'compile-side; only INLINE_SRC_PREFIX is read at render'}*

Modules the walk does NOT enter, each with the reason its change is already
in the key some other way. Everything else it reaches is hashed.

### an.stage.cache_key.RENDER_PATH_ROOT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an.adapters.cutout.render'*

The cut-out renderer’s historical module – since an#247 a live alias of
`an.stage.render`, followed through its `alias_module` target.
The walk is no longer rooted HERE alone: [`render_path_roots()`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_path_roots) derives
the roots from the registered renderer, and this module is one of them.

### an.stage.cache_key.SVG_TEXT_MARKERS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[bytes](https://docs.python.org/3/builtins/stdtypes.html#bytes), ...]* *= (b'<text', b':text')*

A byte sequence that marks an SVG part drawing text with the MACHINE’s fonts.

### an.stage.cache_key.compiled_document(shot, ctx)

The document the stage engine (`StageEngine.open`) will compile for `shot` under `ctx`.

The SAME call, argument for argument — `tests/test_shot_cache.py` pins the
two against each other, so a knob added to one and not the other fails
there rather than in a stale render.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.stage.cache_key.cutout_environment()

The cut-out render’s machine: the bench’s own probes, minus what is not identity.

Chromium’s build and WebGL identity (one browser launch), Playwright, the
full ffmpeg build and the x264 build it encodes with (one 16x16 encode),
the ISA and OS family, and the Python imaging stack the frame stage decodes
and resolves with. The executable PATH is left out — it names a home
directory, not a build. A failed probe is recorded as its error, never as
“fine”.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.stage.cache_key.cutout_shot_inputs(shot, ctx)

The cut-out renderer’s [`ShotKeyer`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyer).

Compiles the shot (timed, as `compile_s`) and digests everything the
render reads beside the document. Compile warnings are re-emitted only when
asked: on a cache MISS the render compiles again and warns itself, so the
engine collects them here (`details["warnings"]`) and replays them only
for a shot it reuses, where they would otherwise never be seen.

* **Return type:**
  [`ShotKeyInputs`](_autosummary/an.build.keys.html.md#an.build.keys.ShotKeyInputs)

### an.stage.cache_key.easing_versions(doc)

`{name: version}` for every registered easing the compiled document names.

A parametrised spec (`cubic-bezier(...)`) or a control-point list carries
its meaning in the document itself and has no version; a name the registry
does not know is left to the compiler, which refuses it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> easing_versions({"animations": {"a": {"channels": [{"keyframes": [
...     {"time": 0, "value": 0, "easing": "ease_in_out"},
...     {"time": 1, "value": 1, "easing": [0.1, 0.2, 0.3, 0.4]}]}]}}})
{'ease_in_out': 1}
```

### an.stage.cache_key.ffmpeg_build()

The whole `ffmpeg -version` (every library’s version and the configure
line), not its first line: the banner is unchanged by `brew upgrade x264`,
which swaps the dynamically linked encoder under it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.stage.cache_key.genre_code_modules()

`{module: source path}` for every module of every package a loaded genre runs code from.

The stage reaches a genre’s code by REGISTRATION (compile passes, lowerings,
entity hooks, services, runtime scripts), never by an import from `an`, so
the static walk from the renderer cannot find it. A package that registers a
hook is therefore keyed WHOLE – every module under it, tests excluded –
which is over-inclusive on purpose: a stale shot-cache hit after a genre
upgrade is the failure (an#294), and a genre’s edit to a module that cannot
reach a pixel costs one re-render. `an` itself is walked from the renderer.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

```pycon
>>> isinstance(genre_code_modules(), dict)
True
```

### an.stage.cache_key.muxed_audio(shot, ctx)

What `_mux_shot` lays under the picture, as data: one entry per muxed line.

Mirrors `_stage_audio_inputs`: a line without an `audio_ref` or a start
is not muxed, and neither is one whose ref the store does not hold. The
bytes’ digest sits beside the ref because the ref keys the SYNTHESIS
inputs, not the bytes, and the mux reads the bytes.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.stage.cache_key.render_code_digest()

sha256 over the source of every module on the render path (by module name).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.stage.cache_key.render_code_modules()

`{module name: sha256 of its source}` for every module on the render path.

What the `code` key part digests, spelled out: a shot cache entry keeps
it, so a re-render can name the modules that changed (an#395).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.stage.cache_key.render_knobs(shot, ctx)

Every `RenderContext` knob, resolved the way the frame stage resolves it.

Validated here too — an invalid `pix_fmt` or supersample factor raises
the render’s own error before anything launches.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.stage.cache_key.render_path_modules(root=None, , excluded={'an.adapters._base': 'the RenderContext/RenderResult types; their values are \`knobs\`', 'an.ir.schema': 'the IR model; what it means for a render reaches \`compiled\`/\`knobs\`', 'an.stage.compile': 'its output is the \`compiled\` part', 'an.stage.serialize': 'its output is the \`compiled\` part', 'an.stage.text_layout': 'compile-side; only INLINE_SRC_PREFIX is read at render'}, moves=None)

`{module: source path}` for every module the render path reaches.

`root` is one module name or several; `None` is [`render_path_roots()`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.render_path_roots).

The walk enters `an` and every package a module MOVED to
(`moves`: `{old: new}`, default `an._shims.declared_moves()`, read
from source). A name under a moved prefix is keyed under its NEW name –
the code that runs – never imported through the old path, and the old
path’s shim file is keyed too (an edit to it changes what the name serves).
So a module that leaves for `cutan` (P8, an#225) stays in the key, under
any warnings filter (review of an#298, S2; an#294).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

```pycon
>>> mods = render_path_modules()
>>> "an.stage.canvas_capture" in mods and "an.stage.compile" not in mods
True
>>> {"an.engines.capture", "an.media.mp4"} <= set(mods)
True
```

### an.stage.cache_key.render_path_roots(renderer_type=None)

The modules a renderer’s render path starts from, read off the renderer itself.

The renderer class’s module, the module its `render` method is defined in
(the core frame stage, for a `FrameStageRenderer`), and its default
engine’s module – so moving the renderer (an#247 PR B) or turning its old
module into a pure shim moves the roots with it. `None` is the cut-out
renderer, plus [`RENDER_PATH_ROOT`](_autosummary/an.stage.cache_key.html.md#an.stage.cache_key.RENDER_PATH_ROOT).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> "an.engines.frame_stage" in render_path_roots()
True
```

### an.stage.cache_key.system_fonts_digest()

A digest of the fonts this machine can draw SVG `<text>` with; once per process.

`fc-list` where fontconfig exists (Linux, and macOS with it installed);
otherwise the listing (name, size, mtime) of the platform’s font folders.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.stage.cache_key.texture_digests(scene_json, mall)

`{alias: sha256 of the bytes staged for it}` for every texture the document declares.

Resolved by the staging step’s own resolver (`an.stage.render.texture_source`),
so what is digested is what is staged. Inline (`data:`) textures are already in the document and
are skipped; anything unresolvable is `ABSENT` with its `src`, so
it still moves the key the day it appears.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.stage.cache_key.x264_build()

The x264 build that ACTUALLY encodes: one 16x16 frame, its SEI read back.

The bench’s own comparability key (`an.bench.environment.x264_sei`), so
the cache and the ledger agree about what “the same encoder” means.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.stage.canvas_capture.html.md

# an.stage.canvas_capture

The canvas capture path: frames read from the page, not photographed off the screen.

The screenshot capture (`render._capture_frames`, `capture="screenshot"`, the
default until an#192) asks Playwright for an ELEMENT screenshot of `#stage` on
every frame. That is a page
capture clipped to the element, taken from the compositor, and it is the largest
single cost in the frame path — ~100 ms/frame at 1080p that is neither the GPU
readback nor the PNG encode (`an-dev-render-pipeline` §8). This path instead
asks the runtime for the canvas’s own pixels (`window.anCaptureFrames` in
`runtime.js`: seek, `app.view.toDataURL('image/png')`), in batches, and does
the rest here.

\*\*It is a different route to the same pixels, and the whole contract is that
nothing downstream can tell.\*\* The frames it writes are RGB PNGs of the declared
size whose DECODED arrays equal the screenshot path’s, so ffmpeg receives the
same frames, the golden gate the same arrays, and the bench the same sizes. The
file bytes differ (a different PNG encoder), which is why the equivalence gate is
on decoded pixels — the same criterion the golden corpus already uses.

The classic traps, and where each one is closed:

- **Row order.** `gl.readPixels` returns rows bottom-up. `toDataURL` returns
  the image top-down, and this path uses `toDataURL` — so there is no flip,
  and adding one is the mutation the equivalence test must catch.
- **Premultiplied alpha.** Not live today: the runtime leaves PixiJS’s
  `backgroundAlpha` at 1, so the WebGL context is created without alpha and
  every pixel measured came back at 255. It becomes live the day a scene gets a
  translucent background — then the premultiplied drawing buffer, the
  un-premultiplied PNG and the screenshot’s composite over the page’s white all
  disagree. So a frame with any pixel below full alpha is REFUSED
  ([`opaque_rgb()`](_autosummary/an.stage.canvas_capture.html.md#an.stage.canvas_capture.opaque_rgb)) rather than guessed at: compositing it here would be a
  second implementation of the browser’s blend, equal to it only by luck.
- \*\*Why not `renderer.extract`.\*\* It re-renders the stage into a render
  texture, which is not multisampled — a different picture from the one the
  `antialias: true` backbuffer holds.
- \*\*Why not raw `readPixels` bytes.\*\* Measured at 1920x1080 on an M1 Max:
  moving 8 MB of RGBA per frame across the DevTools protocol costs 830-950
  ms/frame (in-page base64 through `btoa` or `FileReader`), and Playwright’s
  own typed-array serialisation 5.9 s/frame — against ~46 ms/frame for the PNG
  data URL, which is ~45 KB. The PNG *is* the transfer encoding.

Supersampling and the frame clock go through the same arithmetic as the
screenshot path — `an.adapters.cutout.supersample.block_mean_resolve()` then
`an.adapters.cutout.shutter.temporal_mean()` — so a supersampled or blurred
canvas frame is the screenshot path’s frame, not a lookalike.

Nothing in this module touches a browser: it is the pure half, testable offline.
The loop that drives the page (batching, ordering, back-pressure) is
`render._capture_frames_canvas`.

### Module Attributes

| [`DATA_URL_PREFIX`](_autosummary/an.stage.canvas_capture.html.md#an.stage.canvas_capture.DATA_URL_PREFIX)   | What `HTMLCanvasElement.toDataURL('image/png')` returns.   |
|--------------------------------------------------------------------|------------------------------------------------------------|

### Functions

| [`canvas_frame_png`](_autosummary/an.stage.canvas_capture.html.md#an.stage.canvas_capture.canvas_frame_png)(samples, \*, frame[, ...])   | One output frame's canvas PNGs -> the RGB PNG the screenshot path writes.   |
|------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`decode_data_url`](_autosummary/an.stage.canvas_capture.html.md#an.stage.canvas_capture.decode_data_url)(url, \*, frame)               | A `toDataURL('image/png')` result -> the PNG's bytes, or refuse.            |
| [`opaque_rgb`](_autosummary/an.stage.canvas_capture.html.md#an.stage.canvas_capture.opaque_rgb)(png, \*, frame)                    | Decode a canvas PNG to an RGB `PIL.Image`, refusing any transparency.       |

### Exceptions

| [`CanvasCaptureError`](_autosummary/an.stage.canvas_capture.html.md#an.stage.canvas_capture.CanvasCaptureError)   | A captured frame that cannot be turned into the screenshot path's frame.   |
|-----------------------------------------------------------------------|----------------------------------------------------------------------------|

### *exception* an.stage.canvas_capture.CanvasCaptureError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A captured frame that cannot be turned into the screenshot path’s frame.

### an.stage.canvas_capture.DATA_URL_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'data:image/png;base64,'*

What `HTMLCanvasElement.toDataURL('image/png')` returns. Anything else —
`"data:,"` for a zero-size canvas, or a JPEG a browser fell back to — is
refused rather than decoded as whatever it happens to be.

### an.stage.canvas_capture.canvas_frame_png(samples, , frame, factor=1, size=None, compress_level=None)

One output frame’s canvas PNGs -> the RGB PNG the screenshot path writes.

`samples` are the frame’s instants in capture order (one unless a frame
clock opened the shutter). Each is decoded and refused unless opaque,
resolved by `factor` with the product’s block mean, and the results
averaged with the product’s temporal mean — the arithmetic the screenshot
path runs, so the two agree about every tie. `size` (width, height), when
given, is the DECLARED size the resolved frame must have: a canvas that is
not `factor` times it is refused here, not muxed. `compress_level=None`
reads `DEFAULT_PNG_COMPRESS_LEVEL` at call time.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> import numpy as np
>>> from PIL import Image
>>> def png(a):
...     out = io.BytesIO(); Image.fromarray(a).save(out, format="PNG")
...     return out.getvalue()
>>> big = np.full((4, 6, 4), 255, np.uint8); big[:2, :2, :3] = 0
>>> out = canvas_frame_png([png(big)], frame=0, factor=2, size=(3, 2))
>>> np.asarray(Image.open(io.BytesIO(out))).shape
(2, 3, 3)
>>> canvas_frame_png([png(big)], frame=9, factor=1, size=(3, 2))
Traceback (most recent call last):
  ...
an.stage.canvas_capture.CanvasCaptureError: frame 9: resolved to 6x4, declared 3x2 (supersample 1)
```

### an.stage.canvas_capture.decode_data_url(url, , frame)

A `toDataURL('image/png')` result -> the PNG’s bytes, or refuse.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> decode_data_url(DATA_URL_PREFIX + "iVBORw==", frame=0)[:4]
b'\x89PNG'
>>> decode_data_url("data:,", frame=3)
Traceback (most recent call last):
  ...
an.stage.canvas_capture.CanvasCaptureError: frame 3: the canvas returned 'data:,' rather than a PNG data URL — a zero-size canvas does this
```

### an.stage.canvas_capture.opaque_rgb(png, , frame)

Decode a canvas PNG to an RGB `PIL.Image`, refusing any transparency.

The screenshot path composites the canvas over the page (white, since
`omit_background=False`); where every pixel has alpha 255 that composite
is the identity, and the premultiplied drawing buffer and the
un-premultiplied PNG agree. Anywhere else the two paths would differ, so the
frame is refused with a count rather than blended here.

Pillow end to end, deliberately: the alpha check is `getextrema` and the
drop is `convert("RGB")`, both in C. Slicing `[..., :3]` out of a numpy
view and copying it contiguous measured ~5x slower at 1080p, on a path whose
whole point is time.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> import numpy as np
>>> from PIL import Image
>>> def png(a):
...     out = io.BytesIO(); Image.fromarray(a).save(out, format="PNG")
...     return out.getvalue()
>>> rgba = np.zeros((2, 3, 4), np.uint8); rgba[..., 3] = 255; rgba[0, 0, 0] = 7
>>> np.asarray(opaque_rgb(png(rgba), frame=0))[0, 0].tolist()
[7, 0, 0]
>>> rgba[1, 2, 3] = 128
>>> opaque_rgb(png(rgba), frame=5)
Traceback (most recent call last):
  ...
an.stage.canvas_capture.CanvasCaptureError: frame 5: 1 of 6 canvas pixels are not opaque (min alpha 128); the screenshot path would composite them over the page's white and this path will not guess at that blend
```


# _autosummary/an.stage.compile.html.md

# an.stage.compile

Compile a top-level `Shot` (renderer=”cutout”) into a `CutoutSceneJSON`.

This is the bridge between the renderer-agnostic `an.ir` types and the
cutout-specific JSON contract that the JS runtime will consume in Phase 2B.

Strategy:

1. **Resolve entities** from the project mall: each `AssetRef` in
   `shot.entities` becomes a sub-tree of the cutout scene (a character with
   placeholder rect parts when the character store has no sidecar art yet).
2. **Flatten authoring actions** via `an.ir.compose.flatten` — every
   `tween`/`set`/`play`/composition produces leaf 

   ```
   `
   ```

   FlatAction\`s with
   absolute times.
3. **Compile each FlatAction to PlacedClipJSON entries** on the appropriate
   track. Tween → a 2-keyframe AnimationClipJSON + a PlacedClipJSON (or, under
   `step_hz`, a grid of step-eased keyframes — an#89). Set →
   a step channel that HOLDS until the next action on the same
   target/property. Play → a per-instance clip (`__play__{n}`) resolved
   from the target’s descriptor animation (an#7).

The compiler is deterministic and side-effect-free (it doesn’t write to the
mall). It reads only.

```pycon
>>> from an.ir.schema import Meta, SceneIR, Shot
>>> from an.stage.compile import compile_shot
>>> shot = Shot(id="s1", renderer="cutout", duration=2.0)
>>> j = compile_shot(shot, mall={"characters": {}})
>>> j.timeline.duration
2.0
```

### Module Attributes

| [`RUNTIME_APPLIED_PROPERTIES`](_autosummary/an.stage.compile.html.md#an.stage.compile.RUNTIME_APPLIED_PROPERTIES)   | Every property name the JS runtime's `applyProperty` STATIC switch implements — exactly the numeric transform vocabulary (the rest-value SSOT above).                                                                     |
|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DFLT_TARGET_SUGGESTIONS`](_autosummary/an.stage.compile.html.md#an.stage.compile.DFLT_TARGET_SUGGESTIONS)      | How many "did you mean" paths an unknown-target message offers.                                                                                                                                                           |
| [`CAMERA_NODE`](_autosummary/an.stage.compile.html.md#an.stage.compile.CAMERA_NODE)                  | indexed by the runtime, absent from the tree.                                                                                                                                                                             |
| [`AFTER_BANDS_PRODUCT`](_autosummary/an.stage.compile.html.md#an.stage.compile.AFTER_BANDS_PRODUCT)          | the [`AfterBand`](_autosummary/an.stage.compile.html.md#an.stage.compile.AfterBand) wrappers the scene pass built.                                                                                                                             |
| [`CAMERA_KEYS_PRODUCT`](_autosummary/an.stage.compile.html.md#an.stage.compile.CAMERA_KEYS_PRODUCT)          | The product key under which the follow pass (an#445) leaves the camera keys it resolved from the compiled motion; the camera and parallax passes read them there instead of `camera_keys(shot)`.                          |
| [`STAGE_COMPILE_PASSES`](_autosummary/an.stage.compile.html.md#an.stage.compile.STAGE_COMPILE_PASSES)         | The STAGE's own compile passes, in order.                                                                                                                                                                                 |
| [`RUNTIME_FIELD_KINDS`](_autosummary/an.stage.compile.html.md#an.stage.compile.RUNTIME_FIELD_KINDS)          | The field kinds `runtime.js` implements (its `FIELD_KINDS` table; a test pins the two).                                                                                                                                   |
| [`BAND_INFIX`](_autosummary/an.stage.compile.html.md#an.stage.compile.BAND_INFIX)                   | The infix of a container holding a later band of an environment's planes (an#344): `<env>__band_<k>`, `scope=<env>`.                                                                                                      |
| [`AFTER_INFIX`](_autosummary/an.stage.compile.html.md#an.stage.compile.AFTER_INFIX)                  | The infix of a wrapper carrying a plane's parallax for the entities placed right after it (an#344): `<env>__after_<k>`, `scope=""`.                                                                                       |
| [`SYNTHETIC_MARK`](_autosummary/an.stage.compile.html.md#an.stage.compile.SYNTHETIC_MARK)               | What a synthetic node's name may contain and an entity id may not (an#344).                                                                                                                                               |
| [`STAGE_SCENE_BUILDERS`](_autosummary/an.stage.compile.html.md#an.stage.compile.STAGE_SCENE_BUILDERS)         | phase 0 the backdrop, phase 1 the cast.                                                                                                                                                                                   |
| [`ENVIRONMENT_ART_PREFIX`](_autosummary/an.stage.compile.html.md#an.stage.compile.ENVIRONMENT_ART_PREFIX)       | The `assets.textures` `src` prefix an environment plate is addressed under.                                                                                                                                               |
| [`PLANE_FILL_SPAN`](_autosummary/an.stage.compile.html.md#an.stage.compile.PLANE_FILL_SPAN)              | A `fill` plane with no declared size covers the canvas at any camera scale — defined beside the schema (`an.stage.environments.PLANE_FILL_SPAN`) so the IR layer's framing check reads the same number, re-exported here. |
| [`FOREGROUND_SUFFIX`](_autosummary/an.stage.compile.html.md#an.stage.compile.FOREGROUND_SUFFIX)            | Suffix for the container holding an environment's FOREGROUND planes.                                                                                                                                                      |
| [`STAGE_NODE_SPACE`](_autosummary/an.stage.compile.html.md#an.stage.compile.STAGE_NODE_SPACE)             | The property space every compiled node lives in ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)).                                                                              |
| [`PLANE_EDGE_ON_MARGIN`](_autosummary/an.stage.compile.html.md#an.stage.compile.PLANE_EDGE_ON_MARGIN)         | at ±π/2 the runtime draws nothing, and a value past it is almost always DEGREES typed where radians were meant (an#314 review).                                                                                           |

### Functions

| [`after_problems`](_autosummary/an.stage.compile.html.md#an.stage.compile.after_problems)(shot, mall, \*[, planes_known])   | `[(entity index, why)]` for every `stage.after` the compiler would refuse (an#344): an anchor the shot does not have, the entity itself, a cycle.                                                        |
|---------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`camera_follow_keys`](_autosummary/an.stage.compile.html.md#an.stage.compile.camera_follow_keys)(state)                        | The keys of `state.shot`'s follow camera, one per frame (an#445).                                                                                                                                        |
| [`camera_keys`](_autosummary/an.stage.compile.html.md#an.stage.compile.camera_keys)(shot, \*, width, height)             | [`an.ir.camera.camera_keys()`](_autosummary/an.ir.camera.html.md#an.ir.camera.camera_keys), with its refusal typed for this adapter.                                                           |
| [`compile_passes_for_stage`](_autosummary/an.stage.compile.html.md#an.stage.compile.compile_passes_for_stage)()                       | The stage's passes and every registered one, in run order (stable by name).                                                                                                                              |
| [`compile_shot`](_autosummary/an.stage.compile.html.md#an.stage.compile.compile_shot)(shot[, mall, fps, width, ...])      | Compile a single cutout-style `Shot` to its JS-runtime JSON form.                                                                                                                                        |
| [`duplicate_paths`](_autosummary/an.stage.compile.html.md#an.stage.compile.duplicate_paths)(root[, overlay])                 | Paths the runtime would index twice (its `nodeIndex` is later-wins, so the first node would silently stop answering).                                                                                    |
| [`entity_spaces_of`](_autosummary/an.stage.compile.html.md#an.stage.compile.entity_spaces_of)(shot)                           | `{entity id: space}` for each entity whose kind declares a space other than the kernel default -- what the compiled document records so the default evaluator agrees with validate and compile (an#245). |
| [`foreground_node_name`](_autosummary/an.stage.compile.html.md#an.stage.compile.foreground_node_name)(entity_id)                  | The node name an environment's foreground planes live under.                                                                                                                                             |
| [`node_path_suggestions`](_autosummary/an.stage.compile.html.md#an.stage.compile.node_path_suggestions)(target, paths, \*[, n])    | The built node paths a mistyped `target` most plausibly meant.                                                                                                                                           |
| [`note_raster_rig`](_autosummary/an.stage.compile.html.md#an.stage.compile.note_raster_rig)(entity, desc_data, pack, raster) | Record a rig with raster parts that a colour-setting pack is applied to.                                                                                                                                 |
| [`parse_tint`](_autosummary/an.stage.compile.html.md#an.stage.compile.parse_tint)(value, \*, where)                     | A `#rrggbb` string to three multipliers in 0..1.                                                                                                                                                         |
| [`plane_parents`](_autosummary/an.stage.compile.html.md#an.stage.compile.plane_parents)(env, entity_id)                    | `{plane name: the node path its channels must target}`.                                                                                                                                                  |
| [`retire_front_spelling`](_autosummary/an.stage.compile.html.md#an.stage.compile.retire_front_spelling)(shot)                      | `shot` with every `<env>__front/<plane>` target spelled `<env>/<plane>`, and the `(old, new)` pairs rewritten.                                                                                           |
| [`scene_builders`](_autosummary/an.stage.compile.html.md#an.stage.compile.scene_builders)()                                 | `{entity kind: builder}`: the stage's, and every registered one.                                                                                                                                         |
| [`space_definitions`](_autosummary/an.stage.compile.html.md#an.stage.compile.space_definitions)(entity_spaces)                 | `{space name: definition}` for every space `entity_spaces` names -- what the compiled document embeds as `meta.spaces` so `runtime.js` evaluates each declared entity in its space (an#287).             |
| [`stage_replacements`](_autosummary/an.stage.compile.html.md#an.stage.compile.stage_replacements)()                             | `{stage pass or builder: the genre replacing it}` -- recorded in the compiled document's `meta.extensions` when non-empty.                                                                               |
| [`step_times`](_autosummary/an.stage.compile.html.md#an.stage.compile.step_times)(start, duration, step_hz)             | Clip-local times at which a stepped tween updates its pose (an#89).                                                                                                                                      |
| [`style_pack_for`](_autosummary/an.stage.compile.html.md#an.stage.compile.style_pack_for)(scene_meta, styles_store)         | The `StylePack` a scene declares, or `None` (an#112).                                                                                                                                                    |
| [`unknown_target_message`](_autosummary/an.stage.compile.html.md#an.stage.compile.unknown_target_message)(target, paths)            | One sentence saying `target` is not a built node, with suggestions.                                                                                                                                      |

### Classes

| [`ActionLowering`](_autosummary/an.stage.compile.html.md#an.stage.compile.ActionLowering)(\*args, \*\*kwargs)           | How a genre's action kind becomes clips in the stage compiler (an#225).   |
|-----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`AfterBand`](_autosummary/an.stage.compile.html.md#an.stage.compile.AfterBand)(wrapper, environment, plane)       | A wrapper the parallax pass gives `plane`'s compensation (an#344).        |
| [`CompileState`](_autosummary/an.stage.compile.html.md#an.stage.compile.CompileState)(shot, mall, fps, width, height) | What the stage compiler's passes read and write, for one shot (an#247).   |
| [`SceneBuild`](_autosummary/an.stage.compile.html.md#an.stage.compile.SceneBuild)(shot, mall, textures, ...[, ...]) | The scene being built, as an entity builder sees it (an#247).             |

### Exceptions

| [`CompilePassCollision`](_autosummary/an.stage.compile.html.md#an.stage.compile.CompilePassCollision)   | A genre registered a pass (or a builder) the stage already has, without `replace=True`.   |
|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`CutoutCompileError`](_autosummary/an.stage.compile.html.md#an.stage.compile.CutoutCompileError)     | A shot cannot be compiled to a cutout scene.                                              |
| [`CutoutCompileWarning`](_autosummary/an.stage.compile.html.md#an.stage.compile.CutoutCompileWarning)   | A shot compiles, but something in it will not reach the screen.                           |

### an.stage.compile.AFTER_BANDS_PRODUCT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'after_bands'*

the [`AfterBand`](_autosummary/an.stage.compile.html.md#an.stage.compile.AfterBand) wrappers the scene pass built.

* **Type:**
  `state.products` key

### an.stage.compile.AFTER_INFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '_\_after_'*

The infix of a wrapper carrying a plane’s parallax for the entities placed
right after it (an#344): `<env>__after_<k>`, `scope=""`.

### *class* an.stage.compile.ActionLowering(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

How a genre’s action kind becomes clips in the stage compiler (an#225).

Registered as [`an.genres.ActionKind.lowering`](_autosummary/an.genres.html.md#an.genres.ActionKind.lowering). The stage knows no
kind by name beyond its own (`tween`, `set`); a genre’s kind supplies:

- `extent_resolver(vocab, *, products)`: `action -> seconds` for a leaf
  that names no duration (or `None`);
- `expand(flat_list, *, vocab, fps, step_hz, default_easing, resolutions,
  products)`: the flat list with this kind’s leaves replaced by what they
  stand for.

`products` is the shot’s [`CompileState.products`](_autosummary/an.stage.compile.html.md#an.stage.compile.CompileState.products) (an#348): what a
genre’s earlier compile pass left for later work, by key (a style’s
policy, which decides a walk’s gait and so its length). Read it; the
lowering has no other view of the compile state.

And, per kind:

- `view_of(entity_swaps, vocab, *, duration)`: `flat -> view name | None`
  (or `None`), for kinds whose clips depend on the view in force;
- `clip(action, *, anim_id, vocab, fps, view)`: the animation clip of one
  leaf that survived `expand`.

### *class* an.stage.compile.AfterBand(wrapper, environment, plane)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A wrapper the parallax pass gives `plane`’s compensation (an#344).

### an.stage.compile.BAND_INFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '_\_band_'*

The infix of a container holding a later band of an environment’s planes
(an#344): `<env>__band_<k>`, `scope=<env>`.

### an.stage.compile.CAMERA_KEYS_PRODUCT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'camera_keys'*

The product key under which the follow pass (an#445) leaves the camera keys
it resolved from the compiled motion; the camera and parallax passes read
them there instead of `camera_keys(shot)`.

### an.stage.compile.CAMERA_NODE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'root'*

indexed by the runtime, absent from the tree.

* **Type:**
  The runtime’s camera node

### *exception* an.stage.compile.CompilePassCollision

Bases: [`CutoutCompileError`](_autosummary/an.stage.compile.html.md#an.stage.compile.CutoutCompileError)

A genre registered a pass (or a builder) the stage already has, without `replace=True`.

### *class* an.stage.compile.CompileState(shot, mall, fps, width, height, strict_assets=False, step_hz=None, default_easing=None, style_pack=None, expression_provider=None, textures=<factory>, resolutions=<factory>, overlay_children=<factory>, fonts=<factory>, scene_root=None, vocab=None, animations=<factory>, tracks=<factory>, entity_swaps=<factory>, extra_actions=<factory>, no_lip_sync=frozenset({}), poses=None, view_spans=None, blink_phases=<factory>, gaze_seeds=<factory>, products=<factory>, meta_extensions=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What the stage compiler’s passes read and write, for one shot (an#247).

The stage’s own passes ([`STAGE_COMPILE_PASSES`](_autosummary/an.stage.compile.html.md#an.stage.compile.STAGE_COMPILE_PASSES)) and every pass a genre
registers for the `"stage"` compiler ([`an.genres.CompilePass`](_autosummary/an.genres.html.md#an.genres.CompilePass)) run
over one of these, in order; `_assemble_document()` turns it into the
wire document. The fields are the locals `compile_shot` used to thread by
hand, unchanged, so the document is byte-identical.

#### extra_actions *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)*

Actions a pass contributes beside the authored ones, compiled with them
by the `actions` pass (the cut-out `speech` pass’s pulses, an#248).

#### no_lip_sync *: [Any](https://docs.python.org/3/library/typing.html#typing.Any)* *= frozenset({})*

Cut-out passes’ products, read by later passes (an empty default is what
a shot with no character produces anyway).

#### products *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

what a genre’s pass produces for a later
pass (`products`, by key), and what it adds to the document’s meta
(`meta_extensions` -> `meta.extensions`, omitted when empty). A new
genre needs no new field here.

* **Type:**
  The generic slots (an#247)

### *exception* an.stage.compile.CutoutCompileError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A shot cannot be compiled to a cutout scene. Carries actionable detail.

A `ValueError` rather than a `RuntimeError` — unlike the rest of this
package’s error tree — because every one of these is “this value is not in
the known set”, which is the existing idiom for argument-level rejection.
The render-time errors stay `RuntimeError`: they are failures of the
machinery, not of the input.

### *exception* an.stage.compile.CutoutCompileWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A shot compiles, but something in it will not reach the screen.

The line between this and [`CutoutCompileError`](_autosummary/an.stage.compile.html.md#an.stage.compile.CutoutCompileError) is whether the author
could plausibly have meant it. An unknown `camera.move` is always a
mistake, so it raises. A speaker with no mouth is usually an off-screen
narrator and occasionally a typo — refusing it would break the documented
idiom, and passing in silence is what this whole change is against.

### an.stage.compile.DFLT_TARGET_SUGGESTIONS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 3*

How many “did you mean” paths an unknown-target message offers.

### an.stage.compile.ENVIRONMENT_ART_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'environments/'*

The `assets.textures` `src` prefix an environment plate is addressed under.

### an.stage.compile.FOREGROUND_SUFFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '_\_front'*

Suffix for the container holding an environment’s FOREGROUND planes.

The split has to produce two sibling containers, and before an#110’s review
both were named `entity.id`. The runtime’s `nodeIndex` is a flat
`path -> container` dict, so the second silently overwrote the first — an
authored `set street:x` then moved the foreground half and left the
background where it was, with no warning from either side: the compiler’s
collision check compares `(entity/plane, prop)` and never sees `(entity, x)`,
and the runtime’s unknown-target throw does not fire because the name IS
known, just bound to the wrong one of two. The determinism report’s
`node_count` under-counted by one per split environment too.

### an.stage.compile.PLANE_EDGE_ON_MARGIN *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.001*

at
±π/2 the runtime draws nothing, and a value past it is almost always
DEGREES typed where radians were meant (an#314 review).

* **Type:**
  How close to edge-on a plane may be authored (radians short of ±π/2)

### an.stage.compile.PLANE_FILL_SPAN *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 4000.0*

A `fill` plane with no declared size covers the canvas at any camera scale
— defined beside the schema (`an.stage.environments.PLANE_FILL_SPAN`) so the IR
layer’s framing check reads the same number, re-exported here.

### an.stage.compile.RUNTIME_APPLIED_PROPERTIES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'alpha', 'dash_offset', 'perspective', 'pivot_x', 'pivot_y', 'plane_fade_end', 'plane_fade_start', 'rotation', 'rotation_rad', 'rotation_x', 'scale_x', 'scale_y', 'skew_x', 'skew_y', 'tint_b', 'tint_g', 'tint_r', 'trim_end', 'trim_start', 'x', 'y'})*

Every property name the JS runtime’s `applyProperty` STATIC switch
implements — exactly the numeric transform vocabulary (the rest-value SSOT
above). This is the Python side of the two-evaluator drift gate:
`tests/test_loud_discards.py` extracts the runtime’s actual switch cases
and asserts exact equality with this set, in both directions. It replaced
`pose.py`’s allow-list when the Python applier was deleted (an#86).
Any OTHER property is a swap-set name, applied dynamically through the
node’s `asset_sets` projection (an#87) — `viseme` left the static
switch when that landed, which is precisely what makes it a conventional
set name rather than control flow.

### an.stage.compile.RUNTIME_FIELD_KINDS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'angle', 'color', 'discrete', 'number', 'orbit', 'quaternion', 'vector'})*

The field kinds `runtime.js` implements (its `FIELD_KINDS` table; a test
pins the two). A declared space using any other kind cannot be drawn by the
stage, so the compiler refuses it instead of the browser failing mid-render.

### an.stage.compile.STAGE_COMPILE_PASSES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CompilePass](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass), ...]* *= (CompilePass(name='scene', run=<function \_scene_pass>, order=100, compiler='stage', builds=None, description='the scene tree, overlay, grain, vocabulary', replace=False), CompilePass(name='counters', run=<function \_counters_pass>, order=195, compiler='stage', builds=None, description='counter text blocks: \`value\` -> a set of strings (an#342)', replace=False), CompilePass(name='actions', run=<function \_actions_pass>, order=200, compiler='stage', builds=None, description='authored actions -> clips', replace=False), CompilePass(name='camera_follow', run=<function \_camera_follow_pass>, order=550, compiler='stage', builds=None, description="a following camera's keys, from the compiled motion (an#445)", replace=False), CompilePass(name='camera', run=<function \_camera_pass>, order=600, compiler='stage', builds=None, description='the camera onto the scene root', replace=False), CompilePass(name='parallax', run=<function \_parallax_pass>, order=700, compiler='stage', builds=None, description="planes' parallax", replace=False), CompilePass(name='checks', run=<function \_checks_pass>, order=900, compiler='stage', builds=None, description='targets, easings, stand-ins', replace=False))*

The STAGE’s own compile passes, in order. A genre adds passes between them by
registering [`an.genres.CompilePass`](_autosummary/an.genres.html.md#an.genres.CompilePass) objects for the `"stage"`
compiler; the in-repo cut-out genre registers `swap_pose` (300),
`view_spans` (310), `visemes` (400) and `face` (500), and the `rig`
entity builder (`an.genres.cutout.CUTOUT_COMPILE_PASSES`). Held HERE, not in the genre tables, so
no `without_genres()` can take the stage’s own passes away.

### an.stage.compile.STAGE_NODE_SPACE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'stage.node'*

The property space every compiled node lives in ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)).

### an.stage.compile.STAGE_SCENE_BUILDERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [CompilePass](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass)]* *= {'environment': CompilePass(name='environment', run=<function \_build_environment_entity>, order=0, compiler='stage', builds='environment', description="an environment's planes (the backdrop)", replace=False), 'prop': CompilePass(name='prop', run=<function \_build_prop_entity>, order=1, compiler='stage', builds='prop', description='a prop, a stroked path or a text block', replace=False)}*

phase 0 the backdrop, phase 1 the cast.

* **Type:**
  The stage’s own entity builders

### an.stage.compile.SYNTHETIC_MARK *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '_\_'*

What a synthetic node’s name may contain and an entity id may not (an#344).

### *class* an.stage.compile.SceneBuild(shot, mall, textures, resolutions, style_pack, overlay, fonts, width, height, children=<factory>, in_front=<factory>, reached=<factory>, skipped=<factory>, raster=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The scene being built, as an entity builder sees it (an#247).

A builder ([`an.genres.CompilePass`](_autosummary/an.genres.html.md#an.genres.CompilePass) with `builds` set) appends its
entity’s subtree to `children` (or `overlay`, or `in_front`) and
records its textures and resolutions here, exactly as the scene pass did
by hand.

### an.stage.compile.after_problems(shot, mall, , planes_known=True)

`[(entity index, why)]` for every `stage.after` the compiler would
refuse (an#344): an anchor the shot does not have, the entity itself, a
cycle. The one statement both `compile_shot` and `an validate` read.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> from an.ir.schema import AssetRef, StagePlacement
>>> shot = Shot(id="s", renderer="stage", duration=1.0, entities=[
...     AssetRef(kind="prop", id="a", store="props", ref="a", stage=StagePlacement(after="b")),
...     AssetRef(kind="prop", id="b", store="props", ref="b", stage=StagePlacement(after="a"))])
>>> [why.split(":")[0] for _, why in after_problems(shot, {})]
["entity 'a' is placed after 'b'", "entity 'b' is placed after 'a'"]
```

### an.stage.compile.camera_follow_keys(state)

The keys of `state.shot`’s follow camera, one per frame (an#445).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`CameraKey`](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraKey)]

### an.stage.compile.camera_keys(shot, , width, height)

[`an.ir.camera.camera_keys()`](_autosummary/an.ir.camera.html.md#an.ir.camera.camera_keys), with its refusal typed for this adapter.

The resolver itself lives in the IR layer so `an.ir.validate` can call the
SAME function — one table, not two reconciled by a test. This wrapper only
re-raises `CameraError` as a `CutoutCompileError`, which is the compiler’s
own boundary contract: every failure out of `compile_shot` is one type.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`CameraKey`](_autosummary/an.ir.schema.html.md#an.ir.schema.CameraKey)]

### an.stage.compile.compile_passes_for_stage()

The stage’s passes and every registered one, in run order (stable by name).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`CompilePass`](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.stage.compile.compile_shot(shot, mall=None, , fps=30, width=1920, height=1080, background='#ffffff', strict_assets=False, step_hz=None, expression_provider=None, style_pack=None, default_easing=None)

Compile a single cutout-style `Shot` to its JS-runtime JSON form.

`default_easing` (an#166) is the scene’s `meta.default_easing`: the
curve of every authored tween that names none (tween > this > the built-in
`"ease_in_out"`, [`resolved_easing()`](_autosummary/an.ir.schema.html.md#an.ir.schema.TweenAction.resolved_easing)).
`None` leaves the document byte-identical to before the knob existed.

`expression_provider` (an#98) is the seam that turns authored
`expression` leaves and dialogue `[emotion]` sugar into per-axis
curves for the face solver; `None` is the genre’s default provider.

`step_hz` (an#89) resamples every authored **tween** onto a SHOT-wide
pose grid of that many updates per second (multiples of `1/step_hz` on
this shot’s clock, shared by every tween in the shot; the grid restarts at
a cut), each keyframe step-eased, so the character holds each pose for the
frames between grid points — “on twos” at half the frame rate, “on threes”
at a third. It is sample-and-hold of the eased curve at the grid instants,
not a retiming into holds and fast transitions. `None` (default) leaves
tweens smooth and the compiled document byte-identical to before the knob
existed; anything else must satisfy `0 < step_hz <= fps` — checked HERE
as well as by `an validate`, because a render never runs validate and a
non-positive rate used to spin `step_times` forever (an#89 review).
Exempt by construction, because they are separate
emission sites rather than string-sniffed: the camera (`_add_camera_clips`
— a stepped character under a translating camera slides in screen space,
which is why the practice keeps cameras on ones), compiled blinks, `play`
clips, and swap channels (already stepped by format). The value is
stamped into `meta.step_hz` — only when set — so a serialized scene
declares its timing policy without moving the contract hash of one that
has none.

`strict_assets` turns a stand-in asset — the placeholder rig drawn for a
character whose descriptor is missing, or the default backdrop drawn for an
unknown environment ref — from a warning into a [`CutoutCompileError`](_autosummary/an.stage.compile.html.md#an.stage.compile.CutoutCompileError).
Off by default so an asset-less project still renders; on for anything that
measures pixels, where a stand-in is a wrong answer wearing a right one’s
clothes (an#33).

* **Return type:**
  [`CutoutSceneJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.CutoutSceneJSON)

### an.stage.compile.duplicate_paths(root, overlay=())

Paths the runtime would index twice (its `nodeIndex` is later-wins,
so the first node would silently stop answering). Over the FINISHED tree,
scoped containers included (an#344).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> duplicate_paths(NodeJSON(name="root", children=[NodeJSON(name="a"), NodeJSON(name="a")]))
['a']
```

### an.stage.compile.entity_spaces_of(shot)

`{entity id: space}` for each entity whose kind declares a space other
than the kernel default – what the compiled document records so the
default evaluator agrees with validate and compile (an#245).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from an.ir.schema import AssetRef, Shot
>>> entity_spaces_of(Shot(id="s", entities=[AssetRef(kind="prop", id="p", store="props", ref="p")]))
{}
```

### an.stage.compile.foreground_node_name(entity_id)

The node name an environment’s foreground planes live under.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> foreground_node_name("street")
'street__front'
```

### an.stage.compile.node_path_suggestions(target, paths, , n=3)

The built node paths a mistyped `target` most plausibly meant.

The paths of the SAME entity that end in the same part name — the usual
mistake is a missing level (`ned/left_brow` for `ned/head/left_brow`)
— or, when there are none, the closest spellings.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> built = ["ned", "ned/head", "ned/head/left_brow", "ned/head/mouth", "ned/arm_l"]
>>> node_path_suggestions("ned/left_brow", built)
['ned/head/left_brow']
>>> node_path_suggestions("ned/arm_x", built)
['ned/arm_l']
>>> node_path_suggestions("zzz", built)
[]
```

### an.stage.compile.note_raster_rig(entity, desc_data, pack, raster)

Record a rig with raster parts that a colour-setting pack is applied to.

Public (an#338) because a genre that builds a rig calls it before
[`an.stage.rig.build_rig_subtree()`](_autosummary/an.stage.rig.html.md#an.stage.rig.build_rig_subtree), as the prop builder does.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.stage.compile.parse_tint(value, , where)

A `#rrggbb` string to three multipliers in 0..1.

Per-channel in **sRGB** — on the 8-bit values as written — not linear-light.
`tint` is a multiply the GPU applies in the space the author read the hex
out of, and a fade between two hex values that did not pass through the
values between them would surprise whoever wrote them (an#62).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.stage.compile.plane_parents(env, entity_id)

`{plane name: the node path its channels must target}`.

One rule, computed the same way by the builder and by the parallax pass —
the alternative is two places deciding which container a plane ended up in,
which is the class of drift this wave keeps closing.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from an.stage.environments import EnvironmentDescriptor, Plane
>>> env = EnvironmentDescriptor(name="e", planes=[Plane(name="a"), Plane(name="b")],
...                             characters_after="a")
```

Since an#343 every container an environment is split into indexes its
planes under the environment’s id (`scope`), so the answer no longer
depends on where the environment was cut:

```pycon
>>> plane_parents(env, "street")
{'a': 'street', 'b': 'street'}
```

### an.stage.compile.retire_front_spelling(shot)

`shot` with every `<env>__front/<plane>` target spelled `<env>/<plane>`,
and the `(old, new)` pairs rewritten.

The foreground container indexes its planes under the environment’s id
since an#343, so the old spelling names nothing; indexing the plane twice
would be the an#110 collision. For one cycle the compiler (and `an
validate`) rewrites it with a warning naming the new spelling; a later
issue retires the rewrite.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Shot`](_autosummary/an.ir.schema.html.md#an.ir.schema.Shot), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]]

```pycon
>>> from an.ir.schema import AssetRef, SetAction
>>> shot = Shot(id="s", renderer="stage", duration=1.0,
...             entities=[AssetRef(kind="environment", id="set", store="environments", ref="r")],
...             actions=[SetAction(target="set__front/wall", property="alpha", value=0.5)])
>>> new, pairs = retire_front_spelling(shot)
>>> new.actions[0].target, pairs
('set/wall', [('set__front/wall', 'set/wall')])
```

### an.stage.compile.scene_builders()

`{entity kind: builder}`: the stage’s, and every registered one.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`CompilePass`](_autosummary/an.genres.registry.html.md#an.genres.registry.CompilePass)]

### an.stage.compile.space_definitions(entity_spaces)

`{space name: definition}` for every space `entity_spaces` names –
what the compiled document embeds as `meta.spaces` so `runtime.js`
evaluates each declared entity in its space (an#287).

The definition is `to_json()` without
its prose (a reworded description must not move a contract hash; a
space’s `version` does). A space using a field kind the runtime does not
implement is refused here ([`CutoutCompileError`](_autosummary/an.stage.compile.html.md#an.stage.compile.CutoutCompileError)).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

```pycon
>>> space_definitions({})
{}
>>> space_definitions({"cam": "stage.camera"})["stage.camera"]["fields"][0]
{'pattern': 'x', 'spec': {'kind': 'number'}, 'unit': 'px'}
```

### an.stage.compile.stage_replacements()

`{stage pass or builder: the genre replacing it}` – recorded in the
compiled document’s `meta.extensions` when non-empty.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.stage.compile.step_times(start, duration, step_hz)

Clip-local times at which a stepped tween updates its pose (an#89).

The grid is SHOT-wide — multiples of `1/step_hz` on the shot’s clock,
shared by every tween in the shot — not the tween’s own: “on twos” means
every character changes pose on the same frames, so a tween starting at
0.033 s updates at the next grid point, not 0.033 s later. (Shots compile
independently, so the grid restarts at each cut.) Local 0 (the tween’s
start) and `duration` (where the end value lands) are always present, so
a tween shorter than one step is a single step to its end value.

`step_hz` must be positive: with a non-positive rate the walk below never
reaches `duration` — an infinite loop, not an error — so it is refused.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> step_times(0.0, 0.3, 10)
[0.0, 0.1, 0.2, 0.3]
>>> [round(t, 3) for t in step_times(0.05, 0.3, 10)]
[0.0, 0.05, 0.15, 0.25, 0.3]
>>> step_times(0.0, 0.02, 10)
[0.0, 0.02]
```

### an.stage.compile.style_pack_for(scene_meta, styles_store)

The `StylePack` a scene declares, or `None` (an#112).

`None` for every document written before an#112 and for every scene that
declares no pack — which is what makes this feature byte-identity-free: the
no-pack path is a lookup with a default, not a rewrite.

A declared pack that is missing, or is not a `StylePack`, RAISES. An art
direction the author asked for and did not get is a different picture that
renders happily, which is the an#33 failure this package refuses everywhere
else.

* **Return type:**
  [`StylePack`](_autosummary/an.styles.html.md#an.styles.StylePack) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.stage.compile.unknown_target_message(target, paths)

One sentence saying `target` is not a built node, with suggestions.

Shared by the compiler (which raises it) and `an validate` (which
reports it), so the two say the same thing about the same path.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> print(unknown_target_message("ned/mouth", ["ned", "ned/head", "ned/head/mouth"]))
'ned/mouth' is not a node of the built scene; did you mean 'ned/head/mouth'? (nodes of 'ned': ['ned', 'ned/head', 'ned/head/mouth'])
```


# _autosummary/an.stage.counters.html.md

# an.stage.counters

A counter, lowered at compile: a number that reads as text (an#342, T2 of an#331).

A counter block is a text block whose document declares `counter: {format,
start}` ([`an.stage.text.Counter`](_autosummary/an.stage.text.html.md#an.stage.text.Counter)). In the IR its number is an ordinary
channel, authored as `tween <id> value a→b` and `set <id> value v`; what the
renderer sees is a replacement set (an#341): one `block_0` drawing per string
the frames show, swapped by `set text <key>`. The number never reaches the
runtime, which has no `value` case.

The lowering is the stage’s `counters` compile pass. It runs after every pass
that can contribute a `value` action (a genre pass adds them to
`state.extra_actions` before the `actions` pass) and before the `actions`
pass, whose swap check reads the vocabulary; `tests/test_text_counter.py`
pins that order against the registry. For each counter block it:

1. compiles the block’s `value` leaves as one numeric channel with the
   compiler’s own clip builder (`an.stage.compile._compile_actions()`), so
   easings, `step_hz`, the set-hold rule and a from-less tween’s start are
   exactly those of every other channel;
2. samples that channel on the frame grid `i / fps` (a `step_hz` shot’s
   channel is already stepped, so its grid is honoured by construction);
3. formats each sample ([`an.formats`](_autosummary/an.formats.html.md#module-an.formats)), typesets every distinct string once
   and rebuilds the block as a `texts` set whose rest is the first frame’s
   string;
4. replaces each `value` leaf by a `delay` of the same length (so no
   sequence around it moves) and adds a `set <id>/block_0 text <key>` half a
   frame before the first frame showing each new string (the captions’ rule,
   so no float disagreement about `i / fps` moves a boundary a frame).

A counter with no `start` shows its first action’s value until then; a
from-less tween with neither a `start` nor an earlier `value` action, and a
counter with neither a `start` nor any `value` action, are compile errors
naming both. A shot with no counter block is untouched.

### Module Attributes

| [`VALUE_PROPERTY`](_autosummary/an.stage.counters.html.md#an.stage.counters.VALUE_PROPERTY)     | The block-scoped property a counter's number is authored on.   |
|---------------------------------------------------------------------|----------------------------------------------------------------|
| [`COUNTER_KEY_PREFIX`](_autosummary/an.stage.counters.html.md#an.stage.counters.COUNTER_KEY_PREFIX) | What every generated key of a counter's set starts with.       |

### Functions

| [`lower_counters`](_autosummary/an.stage.counters.html.md#an.stage.counters.lower_counters)(state)             | The `counters` pass (see the module docstring).                                                                                    |
|------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| [`counter_blocks`](_autosummary/an.stage.counters.html.md#an.stage.counters.counter_blocks)(shot, props_store) | `{entity id: block}` for every text block of `shot` that declares a counter (an unresolvable document is the builder's to report). |

### an.stage.counters.COUNTER_KEY_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'v_'*

What every generated key of a counter’s set starts with.

### an.stage.counters.VALUE_PROPERTY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'value'*

The block-scoped property a counter’s number is authored on. NOT a global
authorable property (an#342): a genre’s own `value` is untouched.

### an.stage.counters.counter_blocks(shot, props_store)

`{entity id: block}` for every text block of `shot` that declares a
counter (an unresolvable document is the builder’s to report).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), `_CounterBlock`]

### an.stage.counters.lower_counters(state)

The `counters` pass (see the module docstring). A no-op for a shot
with no counter block, which is every shot written before an#342.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/an.stage.easing.html.md

# an.stage.easing

The stage engine’s easing vocabulary — a view of [`an.timing.easing`](_autosummary/an.timing.easing.html.md#module-an.timing.easing).

The curves, their solvers and the full registry live in the timing kernel
([`an.timing.easing`](_autosummary/an.timing.easing.html.md#module-an.timing.easing)). This module keeps the path every stage caller already
imports, and it states what the STAGE implements: `runtime.js` evaluates the
legacy names in [`an.base.EASING_PRESETS`](_autosummary/an.base.html.md#an.base.EASING_PRESETS) and a cubic-Bézier control
4-tuple, nothing else, so [`apply_easing()`](_autosummary/an.stage.easing.html.md#an.stage.easing.apply_easing) here refuses any other name. That
refusal is what `an validate` and the compiler use to say “the evaluators know
this easing” — a curve the kernel knows but the stage runtime does not would
otherwise surface as a throw in the browser.

```pycon
>>> apply_easing("linear", 0.5)
0.5
>>> round(apply_easing("ease_in_out", 0.5), 6)
0.5
>>> round(apply_easing([0.0, 0.0, 1.0, 1.0], 0.5), 6)
0.5
>>> apply_easing("ease-in", 0.5)
Traceback (most recent call last):
 ...
an.timing.easing.UnknownEasingError: unknown easing preset 'ease-in'; known: ['ease', 'ease_in', 'ease_in_out', 'ease_out', 'linear', 'step']
```

### Module Attributes

| [`EASING_FUNCS`](_autosummary/an.stage.easing.html.md#an.stage.easing.EASING_FUNCS)   | The easings the stage runtime implements (`runtime.js` `EASINGS`), each the kernel registry's own curve.   |
|-----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|

### Functions

| [`apply_easing`](_autosummary/an.stage.easing.html.md#an.stage.easing.apply_easing)(spec, t)               | Apply an easing the STAGE implements to `t` in `[0, 1]`.           |
|--------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`cubic_bezier`](_autosummary/an.stage.easing.html.md#an.stage.easing.cubic_bezier)(cx1, cy1, cx2, cy2, t) | an's cubic-Bézier easing at `t` — the `an-bezier-newton-8` solver. |

### an.stage.easing.EASING_FUNCS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[float](https://docs.python.org/3/builtins/functions.html#float)], [float](https://docs.python.org/3/builtins/functions.html#float)]]* *= {'ease': <function \_ease>, 'ease_in': <function \_ease_in>, 'ease_in_out': <function \_ease_in_out>, 'ease_out': <function \_ease_out>, 'linear': <function \_linear>, 'step': <function \_step>}*

The easings the stage runtime implements (`runtime.js` `EASINGS`), each
the kernel registry’s own curve.

### an.stage.easing.apply_easing(spec, t)

Apply an easing the STAGE implements to `t` in `[0, 1]`.

- `None` → linear (passthrough)
- a string preset name (must be a key of `EASING_FUNCS`)
- a 4-element sequence of cubic-Bézier control points (the legacy solver)

Raises `ValueError` for unknown preset names or malformed sequences.

```pycon
>>> apply_easing(None, 0.25)
0.25
>>> apply_easing("step", 0.99)
0.0
>>> apply_easing("step", 1.0)
1.0
```

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### an.stage.easing.cubic_bezier(cx1, cy1, cx2, cy2, t)

an’s cubic-Bézier easing at `t` — the `an-bezier-newton-8` solver.

The curve is defined by P0=(0,0), P1=(cx1,cy1), P2=(cx2,cy2), P3=(1,1).
Given a desired x=t we solve for the matching curve parameter u, then
return the y coordinate, by exactly 8 clamped Newton steps.

Structurally IDENTICAL to runtime.js::cubicBezier on purpose: always 8
iterations, break only on a degenerate derivative, clamp each step. This
function is the spec of that port, and the two are compared bit-for-bit by
the parity battery — an earlier version had an extra 

```
|u_new - u|
```

 < 1e-9
early-convergence break the JS side lacked, which left the two a ULP apart;
a numeric channel lerping large magnitudes amplifies a ULP of easing by
(b - a) (found by the an#86 adversarial review; the loops now match).

It does NOT agree with the CSS solver (`css_cubic_bezier()`) to 1e-9
for every control point, which is why it stays a named legacy solver until
the pixel goldens are re-blessed under the stricter one.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> round(legacy_cubic_bezier(0.0, 0.0, 1.0, 1.0, 0.5), 6)  # linear
0.5
>>> legacy_cubic_bezier(0.42, 0.0, 0.58, 1.0, 0.0), legacy_cubic_bezier(0.42, 0.0, 0.58, 1.0, 1.0)
(0.0, 1.0)
```


# _autosummary/an.stage.environments.html.md

# an.stage.environments

Environments: a stage made of planes, at declared depths.

An environment was three scalars — `sky_color`, `ground_color`, `ground_y` —
merged through an **intersection filter** that silently dropped everything
else. The test pinning that warning used `parallax_layers: 3` as its example,
which says what the shape was for.

An `EnvironmentDescriptor` declares `planes`, and \*\*list order is draw
order\*\*. There is deliberately no `z` integer: the runtime sets no `zIndex`, so
a `z` field would be a second source of truth it could not honour, and two
orderings that can disagree is how the intersecting override got here in the
first place.

**\`depth\` is the parallax factor**, in Godot’s `Parallax2D.scroll_scale`
coordinates — a ratio, not a distance:

| `depth`     | meaning                                                                                                                                       |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| `0.0`       | infinitely far: does not PAN — pinned against a camera<br/>translation. It still scales and rotates with the camera; see<br/>the limit below. |
| `0 < d < 1` | background — Godot’s own sanity range is 0.1 sky → 0.7 forest                                                                                 |
| `1.0`       | the character plane; **emits nothing**, which is today’s<br/>behaviour for everything and why this is byte-identity-free                      |
| `> 1.0`     | foreground: nearer than the characters, moving faster                                                                                         |

**\`depth\` governs translation only, and that is a stated limit rather than an
oversight.** The compensation is on `x`/`y`; `root.scale` and `root.rotation`
multiply the whole composed expression, so no per-plane factor can cancel
them. Measured: a `depth = 0` plane under `push_in` grows 1.0 → 1.25× and
drifts, exactly like the character plane.

Depth-aware zoom is the **dolly**, and the design of record defers it with its
reason: a true dolly grows the foreground ×1.40 while the moon grows ×1.02,
where today’s `push_in` grows both ×1.25 — precisely the uniform zoom the 1937
multiplane camera was built to replace. Until `dolly_in` exists, pair a
`depth = 0` plate with a pan, not a zoom.

Sign trap, pinned here because every surveyed tool disagrees: \*\*larger depth =
nearer = faster.\*\* Unity’s z-derived factor uses the INVERSE convention
(`f_unity ≡ 1 − f_godot`), so a Unity tutorial read while writing this code
will produce a stage that parallaxes backwards.

`characters_after` is how a plane gets IN FRONT of the characters — Rive’s
relative-ordering shape. `None` reproduces the old two-loop behaviour exactly
(every plane behind every character) and dissolves the tie between two planes
that share a depth.

### Module Attributes

| [`PLANE_FILL_SPAN`](_autosummary/an.stage.environments.html.md#an.stage.environments.PLANE_FILL_SPAN)           | A `fill` plane with no declared size covers the canvas at any camera scale.                                                                                                                           |
|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`Rect`](_autosummary/an.stage.environments.html.md#an.stage.environments.Rect)                      | `(left, top, right, bottom)` in scene pixels, `y` down (the stage's axes).                                                                                                                            |
| [`ENVIRONMENT_DOCUMENT_KIND`](_autosummary/an.stage.environments.html.md#an.stage.environments.ENVIRONMENT_DOCUMENT_KIND) | Its own versioned document, registered from the module that owns the schema — the rule `CharacterDescriptor` and `PropDescriptor` both follow, and the reason the registry is keyed per KIND (an#77). |

### Functions

| [`frame_rect`](_autosummary/an.stage.environments.html.md#an.stage.environments.frame_rect)(\*, x, y, zoom, rotation, width, ...)   | The scene region a camera pose shows: centred on the camera, the canvas divided by the zoom, grown to the axis-aligned box of a rolled frame — CONSERVATIVE under roll (the box contains corners the rotated frame does not show, so a plate that covers a rolled view can still be flagged).   |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`plane_rect`](_autosummary/an.stage.environments.html.md#an.stage.environments.plane_rect)(plane, art_size, \*[, camera])          | Where `plane` is drawn, in scene pixels, with the camera at `camera`.                                                                                                                                                                                                                           |
| [`uncovered_part`](_autosummary/an.stage.environments.html.md#an.stage.environments.uncovered_part)(view, covers)                       | The bounding box of the part of `view` no rect in `covers` covers.                                                                                                                                                                                                                              |

### Classes

| [`EnvironmentDescriptor`](_autosummary/an.stage.environments.html.md#an.stage.environments.EnvironmentDescriptor)(\*\*data)   | A stage made of planes.               |
|------------------------------------------------------------------------------------|---------------------------------------|
| [`Plane`](_autosummary/an.stage.environments.html.md#an.stage.environments.Plane)(\*\*data)                   | One layer of the stage, at one depth. |
| [`PlaneArt`](_autosummary/an.stage.environments.html.md#an.stage.environments.PlaneArt)(\*\*data)                | What a plane is made of.              |

### an.stage.environments.ENVIRONMENT_DOCUMENT_KIND *: [DocumentKind](_autosummary/an.ir.html.md#an.ir.DocumentKind)* *= DocumentKind(name='EnvironmentDescriptor', version_field='schema_version', current_version='0.1.0')*

Its own versioned document, registered from the module that owns the schema
— the rule `CharacterDescriptor` and `PropDescriptor` both follow, and the
reason the registry is keyed per KIND (an#77).

**No migration ladder, deliberately.** The obvious one — “today’s free-form
`meta.json` entries become plane-less descriptors” — would migrate nothing,
because a free-form entry ALREADY validates as an `EnvironmentDescriptor`
with no planes: `extra="allow"` carries `description`/`tags`/the colour
scalars through untouched, and every field this model adds has a default.
There is no shape change to make.

It was written and then removed in review (an#110): `_environment_descriptor`
gates on the `kind` tag before migrating, so only a document already written
in the post-an#110 shape could ever have reached it — a registered migration
that runs on nothing, which is the decoration `CLAUDE.md`’s “never register
a migration without a read path that runs it” rule exists to prevent. The
KIND stays registered: it declares where the version field lives, which is
what a future real migration will need.

### *class* an.stage.environments.EnvironmentDescriptor(\*\*data)

Bases: `_EnvModel`

A stage made of planes. Saved as `meta.json` in the environments store.

```pycon
>>> env = EnvironmentDescriptor(name="forest", planes=[
...     Plane(name="sky", depth=0.1),
...     Plane(name="trees", depth=0.6),
...     Plane(name="grass", depth=1.4),
... ], characters_after="trees")
>>> [p.name for p in env.planes]
['sky', 'trees', 'grass']
>>> env.characters_after
'trees'
```

List order is draw order, and `characters_after` names the plane the
characters stand in front of — so `grass` above is a FOREGROUND plane,
drawn over them. `None` puts every plane behind every character, which is
what the two-loop builder did before an#110 and is why an environment that
declares no planes compiles byte-identically.

#### anchors *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]]*

Named stage marks — a horizon is one of them. A dedicated `horizon`
field would be two fields for one fact, which is how the intersecting
override arrived.

#### characters_after *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The plane the characters are drawn in FRONT of. `None` = all planes
behind all characters.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### planes *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[Plane](_autosummary/an.stage.environments.html.md#an.stage.environments.Plane)]*

the runtime sets no
`zIndex`, so a second ordering would be one it could not honour.

* **Type:**
  **LIST ORDER IS DRAW ORDER.** There is no `z` field

#### source *: [AssetSource](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Where this art came from and what its licence obliges. Not decoration:
`an credits` walked ONLY `mall["characters"]`, so the PR that gives
environments art is the PR that closes that hole — otherwise
`an credits` becomes an affirmative false statement about plates.

### an.stage.environments.PLANE_FILL_SPAN *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 4000.0*

A `fill` plane with no declared size covers the canvas at any camera scale.
The same 4000 the preset backdrop uses, and for the same reason — the runtime
centres `root` and applies camera scale, so a huge rect always covers. Lives
here (the schema) so the IR layer’s framing check and the compiler read one
number; `an.stage.compile` re-exports it.

### *class* an.stage.environments.Plane(\*\*data)

Bases: `BaseModel`

One layer of the stage, at one depth.

```pycon
>>> Plane(name="sky", art=PlaneArt(color="#cfe9ff"), depth=0.1).depth
0.1
>>> Plane(name="sky", parallax=(0.2, 0.0)).parallax
(0.2, 0.0)
```

**\`extra=”forbid”\`, unlike the document that holds it.** An
`EnvironmentDescriptor` is `extra="allow"` because the environments store
is a free-form `meta.json` whose natural shape includes `name`,
`description` and `tags` — refusing those would hard-fail ordinary data. A
plane is not free-form: it is a precise instruction to draw something, and
a misspelled key there is the exact failure an#110 exists to remove. The
old override path *silently dropped* every key it did not know, and the
test pinning that warning used `parallax_layers: 3` as its example.

#### anchor *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]*

The art’s anchor within its own box, in 0..1 per axis.

#### depth *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The parallax factor — a RATIO, in Godot’s `Parallax2D.scroll_scale`
coordinates. `1.0` is the character plane and emits nothing. See this
module’s docstring for the table and for the Unity sign trap.

#### factors()

The per-axis parallax factors this plane actually moves by.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> Plane(name="p", depth=0.4).factors()
(0.4, 0.4)
>>> Plane(name="p", depth=0.4, parallax=(0.2, 0.0)).factors()
(0.2, 0.0)
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Becomes the scene node’s name, under the environment entity’s id.

#### offset *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]*

Where the plane sits, in scene pixels from the stage centre.

#### parallax *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per-axis override of `depth`, for a plane that scrolls horizontally but
not vertically. `None` means `(depth, depth)`.

#### size *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`None` = the art’s own extent. A `fill` with no size covers the canvas.
The box the art is fitted into, in scene pixels. **A declared size wins**
(an#211); `None` = the art’s own extent — an SVG’s `width`/`height`, a
raster’s pixel size. A `fill` with no size covers the canvas.

#### source *: [AssetSource](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Where THIS plane’s art came from, when it is not the environment’s —
a composite stage of a carved plate and a CC0 prop credits both
(an#211). `None` = the environment’s `source` covers it. Omitted from
the stored document when unset.

### *class* an.stage.environments.PlaneArt(\*\*data)

Bases: `BaseModel`

What a plane is made of.

```pycon
>>> PlaneArt(kind="fill", color="#cfe9ff").color
'#cfe9ff'
>>> PlaneArt(kind="image", src="plates/forest.svg").src
'plates/forest.svg'
>>> PlaneArt(kind="image", src="plates/street.png").src
'plates/street.png'
```

An `image` is SVG or raster — PNG, JPEG or WebP (an#211): the compiler
sizes it from its header and PixiJS loads it natively.

A `gradient` (an#275) is a linear or radial blend of colour stops
([`an.paint.Gradient`](_autosummary/an.paint.html.md#an.paint.Gradient), CSS’s vocabulary), compiled into an inline SVG
texture ([`an.stage.gradients`](_autosummary/an.stage.gradients.html.md#module-an.stage.gradients)) – a backlit-glass plate or a sky
without hand-drawing one:

```pycon
>>> PlaneArt(kind="gradient", gradient={"type": "radial",
...          "stops": ["#fff3d0", "#3a2a18"]}).gradient.type
'radial'
```

A `role` names the plane’s paint for a StylePack: under a pack whose
`gradients` sets that role, the plane is drawn with the pack’s gradient (a
`fill` plane included); with no pack, or a pack that does not set it, the
plane draws exactly as authored.

```pycon
>>> PlaneArt(kind="fill", color="#202830", role="glass").role
'glass'
```

`generated` art is still not a kind: it needs a generator, and this
package’s standing rule is that schema without a consumer is worse than an
absent field — the `repeat`/`TilingSprite` decision in an#110 is the same
call made the same way.

#### color *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

a CSS colour.

* **Type:**
  `fill` only

#### gradient *: [Gradient](_autosummary/an.paint.html.md#an.paint.Gradient) | [None](https://docs.python.org/3/builtins/constants.html#None)*

the gradient (an#275). Omitted from the stored document
when unset.

* **Type:**
  `gradient` only

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### role *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

the gradient role a StylePack may set for this
plane (an#275). Omitted from the stored document when unset.

* **Type:**
  `fill` or `gradient`

#### src *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

a path under the environment’s own folder in the store,
exactly as a character attachment’s `path` is — `.svg`, `.png`,
`.jpg`/`.jpeg` or `.webp`.

* **Type:**
  `image` only

### an.stage.environments.Rect

`(left, top, right, bottom)` in scene pixels, `y` down (the stage’s axes).

alias of [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.stage.environments.frame_rect(, x, y, zoom, rotation, width, height)

The scene region a camera pose shows: centred on the camera, the canvas
divided by the zoom, grown to the axis-aligned box of a rolled frame —
CONSERVATIVE under roll (the box contains corners the rotated frame does
not show, so a plate that covers a rolled view can still be flagged).

`root.pivot` is the camera and `root.scale` the zoom, composed about the
canvas centre, so a pose shows `camera ± canvas / (2 · zoom)`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> frame_rect(x=0, y=0, zoom=1.25, rotation=0, width=320, height=240)
(-128.0, -96.0, 128.0, 96.0)
```

### an.stage.environments.plane_rect(plane, art_size, , camera=(0.0, 0.0))

Where `plane` is drawn, in scene pixels, with the camera at `camera`.

The compiler’s own geometry, restated for a pre-flight: the box is the
declared `size` or the art’s extent (`art_size`), the art is fitted into
it by `fit`, placed by `anchor` at `offset`, and the plane’s parallax
compensation moves it by `(1 − f) · camera` per axis. `None` when the
extent cannot be known (an image whose art cannot be measured and whose
`fit` makes the drawn size depend on it) — an unknown, not a hole.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> plane_rect(Plane(name="p", art=PlaneArt(kind="image", src="a.png"),
...                  size=(100.0, 50.0), fit="stretch"), None)
(-50.0, -25.0, 50.0, 25.0)
>>> plane_rect(Plane(name="p", art=PlaneArt(kind="image", src="a.png"), depth=0.0),
...            (200.0, 100.0), camera=(40.0, 0.0))
(-60.0, -50.0, 140.0, 50.0)
```

### an.stage.environments.uncovered_part(view, covers)

The bounding box of the part of `view` no rect in `covers` covers.

`None` when the union covers the whole view. Exact for axis-aligned
rects: the view is cut into cells at every cover edge, and a cell is
covered or not as a whole.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> uncovered_part((0, 0, 10, 10), [(0, 0, 10, 8)])
(0, 8, 10, 10)
>>> uncovered_part((0, 0, 10, 10), [(0, 0, 6, 10), (5, 0, 10, 10)]) is None
True
```


# _autosummary/an.stage.fidelity.html.md

# an.stage.fidelity

How faithfully a compiled scene reproduces the art it was built from.

Wave 4’s instrument (#9, `misc/docs/wave4_research.md` §2). One number per
sprite: the **aspect distortion**, the factor by which the compiler’s box
disagrees with the art’s own raster about shape.

## Why this and not a rendered bounding box

The obvious instrument — render a frame, find the part’s ink, measure its
extents — does not work. Measured on the repo’s own art, the same `arm_l`
render is 2, 4, 6 or 8 px wide depending only on the anti-aliasing threshold
chosen, so any tolerance asserted against it is really an assertion about a
threshold. The scale ratio below is exact, threshold-free, needs no browser and
no render at all, and reads straight off the two numbers that actually decide
the shape.

## What it measures

`makeSvgSprite` sets `sprite.width` and `sprite.height` independently
(`runtime.js:164-165`); PixiJS turns each into an independent axis scale.
So the sprite’s two scale factors are

> sx = box_width  / raster_width
> sy = box_height / raster_height

and the art keeps its shape exactly when `sx == sy`.
[`PartFidelity.aspect_distortion`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.PartFidelity.aspect_distortion) is `max(sx, sy) / min(sx, sy)` — 1.0
when the art is respected, and the factor by which it is squashed otherwise.

\*\*The invariant this exists to enforce: aspect ratio is intrinsic to the art,
and the compiler may never override it.\*\* A part is placed and uniformly
scaled, never stretched to fit a box.

### Module Attributes

| [`DFLT_ASPECT_TOLERANCE`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.DFLT_ASPECT_TOLERANCE)   | Ratios within this of 1.0 count as uniform.                                 |
|--------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`SPRITE_KIND`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.SPRITE_KIND)             | The visual kind whose art comes from a file and can therefore be distorted. |
| [`CONTAIN_FIT`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.CONTAIN_FIT)             | Fit policies, mirroring `VisualJSON.fit`.                                   |

### Functions

| [`aspect_findings`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.aspect_findings)(scene, \*, asset_root[, ...])     | Report each non-uniformly scaled sprite as a typed `Finding`.          |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`part_fidelity`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.part_fidelity)(scene, \*, asset_root[, tolerance]) | Measure every `svg_sprite` in a compiled scene against its source art. |

### Classes

| [`PartFidelity`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.PartFidelity)(node_path, asset_id, src, box, ...)   | One sprite's box, its art's raster, and the disagreement between them.   |
|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|

### an.stage.fidelity.CONTAIN_FIT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'contain'*

Fit policies, mirroring `VisualJSON.fit`.

### an.stage.fidelity.DFLT_ASPECT_TOLERANCE *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 1.000001*

Ratios within this of 1.0 count as uniform. Guards float noise only — it is
not a tolerance for “close enough”, which is why it is this tight.

### *class* an.stage.fidelity.PartFidelity(node_path, asset_id, src, box, raster, fit='stretch')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One sprite’s box, its art’s raster, and the disagreement between them.

#### *property* aspect_distortion *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The factor by which the art is actually reshaped on screen.

**The fit policy is what decides this, not the box.** Under
`contain` the runtime scales both axes by one factor, so the art
keeps its shape whatever the box says and this is 1.0. Under
`stretch` the box wins on both axes and the disagreement is the
distortion.

#### *property* box_aspect_disagreement *: [float](https://docs.python.org/3/builtins/functions.html#float)*

How far the box’s shape is from the art’s. 1.0 when they agree.

Pure geometry, independent of the fit policy. Under `contain` this is
*slack* — the art still keeps its shape and the box is simply roomier on
one axis — so it is a weaker signal than [`aspect_distortion`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.PartFidelity.aspect_distortion), but
it is what tells you the compiler is sizing from the art rather than
from a constant.

### an.stage.fidelity.SPRITE_KIND *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'svg_sprite'*

The visual kind whose art comes from a file and can therefore be distorted.

### an.stage.fidelity.aspect_findings(scene, , asset_root, severity='error', tolerance=1.000001)

Report each non-uniformly scaled sprite as a typed `Finding`.

Uses the existing finding type so the orchestrator’s error routing applies,
and sets `ir_path` to the scene-graph node path so a fix is routed to the
part that needs it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/an.verify.html.md#an.verify.Finding)]

### an.stage.fidelity.part_fidelity(scene, , asset_root, tolerance=1.000001)

Measure every `svg_sprite` in a compiled scene against its source art.

`asset_root` is the directory a texture’s `src` is relative to — the
staged runtime directory, or the mall’s characters-store root with the
`characters/` prefix retained.

Sprites whose art cannot be read are skipped rather than guessed at; a
missing part is #76’s problem, not this function’s.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`PartFidelity`](_autosummary/an.stage.fidelity.html.md#an.stage.fidelity.PartFidelity)]


# _autosummary/an.stage.gradients.html.md

# an.stage.gradients

Gradient fills on the stage: a [`Gradient`](_autosummary/an.paint.html.md#an.paint.Gradient) drawn as an inline SVG texture.

The stage draws a gradient the way it already draws a glow (an#163): as ordinary
document content – a `data:` SVG texture under a content-addressed alias,
on an `svg_sprite` visual – not as a runtime feature. So `runtime.js` does
not change, staging skips the inline source, and the bytes of the texture are a
pure function of the gradient and its box.

Two boxes matter:

- the **box** the sprite covers (a plane’s declared `size`, or the fill span
  that covers the canvas at any camera position), and
- the **frame** the gradient is laid out over (the declared `size`, or the
  canvas): a gradient without a declared size is shaped by what the camera
  shows, and beyond the frame its end colours hold (SVG’s `spreadMethod`
  `pad`, CSS’s behaviour), so a pan never runs off the edge of the paint.

The SVG is rasterised at most [`GRADIENT_RASTER_MAX`](_autosummary/an.stage.gradients.html.md#an.stage.gradients.GRADIENT_RASTER_MAX) pixels on its longer
side and stretched to the box – a smooth ramp loses nothing to that, and a
4000-pixel backdrop does not cost a 64 MB texture.

```pycon
>>> from an.paint import Gradient
>>> svg = gradient_svg(Gradient(stops=["#000", "#fff"]), box=(100.0, 50.0), frame=(100.0, 50.0))
>>> svg.startswith('<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"')
True
>>> 'x1="50" y1="0" x2="50" y2="50"' in svg  # CSS's default: top to bottom
True
```

### Module Attributes

| [`GRADIENT_RASTER_MAX`](_autosummary/an.stage.gradients.html.md#an.stage.gradients.GRADIENT_RASTER_MAX)   | The longest side, in texture pixels, a gradient is rasterised at.   |
|------------------------------------------------------------------------|---------------------------------------------------------------------|

### Functions

| [`gradient_svg`](_autosummary/an.stage.gradients.html.md#an.stage.gradients.gradient_svg)(gradient, \*, box, frame[, ...])   | The SVG that paints `gradient` over `box`, laid out on the centred `frame`.   |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`gradient_src`](_autosummary/an.stage.gradients.html.md#an.stage.gradients.gradient_src)(svg)                               | `svg` as the inline (`data:`) texture source the runtime loads.               |
| [`gradient_alias`](_autosummary/an.stage.gradients.html.md#an.stage.gradients.gradient_alias)(prefix, src)                     | A content-addressed texture alias: the same gradient, the same alias.         |

### an.stage.gradients.GRADIENT_RASTER_MAX *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 1024*

The longest side, in texture pixels, a gradient is rasterised at.

### an.stage.gradients.gradient_alias(prefix, src)

A content-addressed texture alias: the same gradient, the same alias.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> gradient_alias("glass.sky", "data:x") == gradient_alias("glass.sky", "data:x")
True
```

### an.stage.gradients.gradient_src(svg)

`svg` as the inline (`data:`) texture source the runtime loads.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.stage.gradients.gradient_svg(gradient, , box, frame, raster_max=1024)

The SVG that paints `gradient` over `box`, laid out on the centred `frame`.

Coordinates are scene pixels (the `viewBox`); `width`/`height` are the
raster size, at most `raster_max` on the longer side.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from an.paint import Gradient
>>> svg = gradient_svg(Gradient(type="radial", stops=["#fff", "#000"]),
...                    box=(4000.0, 4000.0), frame=(1920.0, 1080.0))
>>> 'width="1024" height="1024"' in svg
True
>>> 'gradientTransform="translate(2000 2000) scale(960 540)"' in svg
True
```


# _autosummary/an.stage.html.md

# an.stage

The 2D stage: the default engine, shipped with `an` but outside the core (an#247).

ADR 0001 decisions 5, 9 and 14; core study §5. The stage is a keyframed 2D
scene-graph player – `runtime.js` (PixiJS) in headless Chromium, its JSON
wire contract, and what it draws for every genre: stroked paths, text, raster
and SVG art, planes with parallax, props, the camera, surface treatments. It is
ONE engine among several (Manim draws explainer shots, `previz` engines draw
view states), so the core never imports it: the renderer registry names it by
module and imports it on first use (`an.adapters.register_lazy_renderer`).
Its optional dependency (Playwright) is the `an[stage]` extra.

- [`an.stage.render`](_autosummary/an.stage.render.html.md#module-an.stage.render) – [`StageEngine`](_autosummary/an.stage.render.html.md#an.stage.render.StageEngine) and the
  renderer, which claims both `stage` and the persisted `cutout`;
- [`an.stage.compile`](_autosummary/an.stage.compile.html.md#module-an.stage.compile) – shot -> compiled document, as ordered compile
  passes (`an.stage.passes`); genres register theirs;
- [`an.stage.serialize`](_autosummary/an.stage.serialize.html.md#module-an.stage.serialize) – the wire contract (field names unchanged);
- [`an.stage.timeline`](_autosummary/an.stage.timeline.html.md#module-an.stage.timeline) – the compiled document’s evaluation and screen space;
- drawables: [`paths`](_autosummary/an.stage.paths.html.md#module-an.stage.paths), [`text`](_autosummary/an.stage.text.html.md#module-an.stage.text),
  [`raster`](_autosummary/an.stage.raster.html.md#module-an.stage.raster), [`environments`](_autosummary/an.stage.environments.html.md#module-an.stage.environments), [`props`](_autosummary/an.stage.props.html.md#module-an.stage.props);
  [`path_geometry`](_autosummary/an.stage.path_geometry.html.md#module-an.stage.path_geometry), [`text_layout`](_autosummary/an.stage.text_layout.html.md#module-an.stage.text_layout), [`surface`](_autosummary/an.stage.surface.html.md#module-an.stage.surface);
- [`an.stage.preview`](_autosummary/an.stage.preview.html.md#module-an.stage.preview) (live preview), [`an.stage.fidelity`](_autosummary/an.stage.fidelity.html.md#module-an.stage.fidelity),
  [`an.stage.canvas_capture`](_autosummary/an.stage.canvas_capture.html.md#module-an.stage.canvas_capture), [`an.stage.runtime_files`](_autosummary/an.stage.runtime_files.html.md#module-an.stage.runtime_files) and the bundled
  > runtime under `an/stage/runtime/`.

Every old path (`an.adapters.cutout.render`, `an.paths`, …) is a live alias.
This `__init__` imports nothing, so importing one stage module does not load
the others.

### Module Attributes

| [`STAGE_RENDERER_NAMES`](_autosummary/an.stage.html.md#an.stage.STAGE_RENDERER_NAMES)   | the persisted `cutout` and the engine's own `stage` (ADR 0001 decision 9).   |
|-------------------------------------------------------------------------|------------------------------------------------------------------------------|

### an.stage.STAGE_RENDERER_NAMES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('cutout', 'stage')*

the persisted `cutout` and
the engine’s own `stage` (ADR 0001 decision 9). The ONE copy.

* **Type:**
  The `Shot.renderer` values the stage draws

### Modules

| [`cache_key`](_autosummary/an.stage.cache_key.html.md#module-an.stage.cache_key)           | What a cut-out shot render reads: the keyer behind its shot-cache key (ADR 0004).                                                           |
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| [`canvas_capture`](_autosummary/an.stage.canvas_capture.html.md#module-an.stage.canvas_capture) | The canvas capture path: frames read from the page, not photographed off the screen.                                                        |
| [`compile`](_autosummary/an.stage.compile.html.md#module-an.stage.compile)               | Compile a top-level `Shot` (renderer="cutout") into a `CutoutSceneJSON`.                                                                    |
| [`counters`](_autosummary/an.stage.counters.html.md#module-an.stage.counters)             | A counter, lowered at compile: a number that reads as text (an#342, T2 of an#331).                                                          |
| [`easing`](_autosummary/an.stage.easing.html.md#module-an.stage.easing)                 | The stage engine's easing vocabulary — a view of [`an.timing.easing`](_autosummary/an.timing.easing.html.md#module-an.timing.easing). |
| [`environments`](_autosummary/an.stage.environments.html.md#module-an.stage.environments)     | Environments: a stage made of planes, at declared depths.                                                                                   |
| [`fidelity`](_autosummary/an.stage.fidelity.html.md#module-an.stage.fidelity)             | How faithfully a compiled scene reproduces the art it was built from.                                                                       |
| [`gradients`](_autosummary/an.stage.gradients.html.md#module-an.stage.gradients)           | Gradient fills on the stage: a [`Gradient`](_autosummary/an.paint.html.md#an.paint.Gradient) drawn as an inline SVG texture.  |
| [`path_geometry`](_autosummary/an.stage.path_geometry.html.md#module-an.stage.path_geometry)   | Stroked-path geometry — the executable spec of `runtime.js::pathGeometry`.                                                                  |
| [`path_wobble`](_autosummary/an.stage.path_wobble.html.md#module-an.stage.path_wobble)       | A hand-drawn wobble for stroked paths, applied at compile time (an#161).                                                                    |
| [`paths`](_autosummary/an.stage.paths.html.md#module-an.stage.paths)                   | Stroked paths: routes, invasion arrows, borders, timelines, connectors.                                                                     |
| [`preview`](_autosummary/an.stage.preview.html.md#module-an.stage.preview)               | Live preview server: render a project's current scene in a browser, reloading on edit.                                                      |
| [`prop_validate`](_autosummary/an.stage.prop_validate.html.md#module-an.stage.prop_validate)   | Check a prop's asset folder offline, and say what a prop must be (an#340).                                                                  |
| [`props`](_autosummary/an.stage.props.html.md#module-an.stage.props)                   | Props: a rig whose art is not a person.                                                                                                     |
| [`raster`](_autosummary/an.stage.raster.html.md#module-an.stage.raster)                 | Raster art: what a PNG, JPEG or WebP is, read from its header (an#211).                                                                     |
| [`render`](_autosummary/an.stage.render.html.md#module-an.stage.render)                 | The 2D stage engine (`runtime.js` in headless Chromium), and the cut-out renderer built on it.                                              |
| [`rig`](_autosummary/an.stage.rig.html.md#module-an.stage.rig)                       | The rig model the stage draws: bones, slots, skins and attachments.                                                                         |
| [`runtime`](_autosummary/an.stage.runtime.html.md#module-an.stage.runtime)               | Cutout JS runtime — see README.md in this directory.                                                                                        |
| [`runtime_files`](_autosummary/an.stage.runtime_files.html.md#module-an.stage.runtime_files)   | Locate the bundled cutout JS runtime files.                                                                                                 |
| [`serialize`](_autosummary/an.stage.serialize.html.md#module-an.stage.serialize)           | JSON contract between the Python compiler and the (future) JS runtime.                                                                      |
| [`snapshot`](_autosummary/an.stage.snapshot.html.md#module-an.stage.snapshot)             | Rasterise art files to PNG in the browser the stage already uses (an#347).                                                                  |
| [`surface`](_autosummary/an.stage.surface.html.md#module-an.stage.surface)               | Surface treatments, compiled (an#163 gap 5): outline, paper-gap shadow, glow, grain.                                                        |
| [`text`](_autosummary/an.stage.text.html.md#module-an.stage.text)                     | Words on screen: title cards, labels, and text you can animate word by word.                                                                |
| [`text_layout`](_autosummary/an.stage.text_layout.html.md#module-an.stage.text_layout)       | A text block, compiled: one node per unit, each an SVG sprite (an#155).                                                                     |
| [`timeline`](_autosummary/an.stage.timeline.html.md#module-an.stage.timeline)             | Stage timeline helpers: the compiled scene as a `Timeline`, and screen space.                                                               |
| [`tree`](_autosummary/an.stage.tree.html.md#module-an.stage.tree)                     | The scene tree's paths: the ONE Python statement of how `runtime.js` indexes nodes (an#343).                                                |


# _autosummary/an.stage.path_geometry.html.md

# an.stage.path_geometry

Stroked-path geometry — the executable spec of `runtime.js::pathGeometry`.

The runtime draws a path (an#160) from three things this module defines:

- **arc length** — cumulative straight-segment lengths over the polyline;
- **trim** — the visible span `[min(ts, te), max(ts, te)]`, clamped to
  `[0, 1]`, as fractions of that length;
- **dashes** — an optional on/off pattern laid along the WHOLE path from arc
  length 0 (shifted by an offset) and only then clipped to the trimmed span, so
  a draw-on reveals dashes in place rather than making them crawl (an#161);
- **the arrowhead** — a triangle whose tip is the trimmed end, oriented along
  the direction of the segment the tip lies on (the *incoming* segment when
  the tip sits exactly on a vertex).

`tests/test_path.py` lifts the real `runtime.js` functions, runs them under
node on a shared battery, and compares — behaviourally, like the channel
evaluator’s parity test. Both sides use the **same operation order** and no
trigonometry (a direction is a unit vector, never an angle), and every
operation is IEEE `+ - * / sqrt`, which both languages round exactly — so
the parity is exact, not within a tolerance.

Cubic Béziers never reach the runtime: [`flatten_curve()`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.flatten_curve) turns them into a
polyline at compile time, so the wire carries one geometry kind.

```pycon
>>> g = path_geometry([(0.0, 0.0), (100.0, 0.0)], 0.0, 0.5)
>>> g["stroke"]
[(0.0, 0.0), (50.0, 0.0)]
>>> g["head"] is None
True
>>> g = path_geometry([(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)], 0.0, 0.75,
...                   head_length=20.0, head_width=10.0)
>>> g["head"]  # tip at (100, 50), pointing DOWN (+y) along the second leg
[(100.0, 50.0), (95.0, 30.0), (105.0, 30.0)]
```

### Module Attributes

| [`HEAD_STROKE_INSET`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.HEAD_STROKE_INSET)   | Where the stroke stops under an arrowhead, as a fraction of the head's length back from the tip.                                                                                                            |
|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`OUTLINE_MITER_LIMIT`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.OUTLINE_MITER_LIMIT) | The longest a variable-width stroke's corner may reach, as a multiple of its half-width there (an#161): past it the miter is cut to this length, so a hairpin turn does not throw a spike across the frame. |

### Functions

| [`flatten_curve`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.flatten_curve)(points, \*[, curve, samples, ...])   | The polyline the runtime draws for `points`.                                                                                        |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| [`cumulative_lengths`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.cumulative_lengths)(points)                         | Arc length at each vertex.                                                                                                          |
| [`point_at`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.point_at)(points, cum, s)                           | The point at arc length `s`.                                                                                                        |
| [`trim_polyline`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.trim_polyline)(points, cum, a, b)                   | The sub-polyline between arc lengths `a < b`: the two cut points and every vertex strictly between them.                            |
| [`dash_spans`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.dash_spans)(a, b, dash, gap, offset)                | The arc-length spans `[lo, hi]` inside `[a, b]` that a dash covers.                                                                 |
| [`path_geometry`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.path_geometry)(points, trim_start, trim_end, \*)    | What the runtime draws: `{"stroke": [points], "head": [3 points] | None}`.                                                          |
| [`profile_width`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.profile_width)(profile, width, u)                   | The stroke width at fraction `u` of the WHOLE path's length (an#161): `width` times the profile's factor, linear between its stops. |
| [`stroke_outline`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.stroke_outline)(line, start, total, width, ...)     | The filled polygon of a variable-width stroke along `line` (an#161).                                                                |

### an.stage.path_geometry.HEAD_STROKE_INSET *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.5*

Where the stroke stops under an arrowhead, as a fraction of the head’s
length back from the tip. Half-way keeps a butt or round cap inside the
head for the default proportions, so the stroke never pokes past the tip.

### an.stage.path_geometry.OUTLINE_MITER_LIMIT *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 4.0*

The longest a variable-width stroke’s corner may reach, as a multiple of
its half-width there (an#161): past it the miter is cut to this length, so
a hairpin turn does not throw a spike across the frame.

### an.stage.path_geometry.cumulative_lengths(points)

Arc length at each vertex. Mirror of `runtime.js::pathLengths`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> cumulative_lengths([(0, 0), (3, 4), (3, 10)])
[0.0, 5.0, 11.0]
```

### an.stage.path_geometry.dash_spans(a, b, dash, gap, offset)

The arc-length spans `[lo, hi]` inside `[a, b]` that a dash covers.

The pattern is anchored at arc length `0` — dash `k` covers
`[offset + k*period, offset + k*period + dash]` — and is clipped to the
window afterwards, so moving `a` or `b` never moves a dash. Only IEEE
`+ - * /` and `floor`, in the order `runtime.js::pathDashSpans` uses.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

```pycon
>>> dash_spans(0.0, 100.0, 10.0, 15.0, 0.0)
[(0.0, 10.0), (25.0, 35.0), (50.0, 60.0), (75.0, 85.0)]
>>> dash_spans(30.0, 60.0, 10.0, 15.0, 0.0)  # clipped, not re-anchored
[(30.0, 35.0), (50.0, 60.0)]
>>> dash_spans(0.0, 30.0, 10.0, 10.0, 5.0)  # offset slides the pattern forward
[(5.0, 15.0), (25.0, 30.0)]
```

### an.stage.path_geometry.flatten_curve(points, , curve='polyline', samples=24, sampling='parameter')

The polyline the runtime draws for `points`.

`curve="cubic"` reads `points` as chained cubic Béziers
(`p0 c1 c2 p1 c1 c2 p2 ...`) and samples each at `samples` steps;
shared endpoints appear once. `sampling="parameter"` (the default) steps
uniformly in each curve’s parameter, so points crowd where the curve is
slow; `"arclength"` (an#161) steps uniformly along its length, so the
points are evenly spaced (a smoother bend for the same count).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

```pycon
>>> flatten_curve([(0, 0), (10, 0)])
[(0.0, 0.0), (10.0, 0.0)]
>>> pts = flatten_curve([(0, 0), (0, 10), (10, 10), (10, 0)], curve="cubic", samples=2)
>>> pts
[(0.0, 0.0), (5.0, 7.5), (10.0, 0.0)]
```

### an.stage.path_geometry.path_geometry(points, trim_start, trim_end, , head_length=0.0, head_width=0.0, dash=0.0, gap=0.0, dash_offset=0.0, tail_head_length=0.0, tail_head_width=0.0, width=0.0, width_profile=None)

What the runtime draws: `{"stroke": [points], "head": [3 points] | None}`.

`tail_head_length > 0` adds a TAIL arrowhead at the trimmed start,
pointing back along the path (an#161: a double-headed arrow with both); it
is then under a `"tail"` key (absent otherwise), and the stroke starts
[`HEAD_STROKE_INSET`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.HEAD_STROKE_INSET) of its length in from that tip. While the visible
length is shorter than the heads together, both are scaled by the same
factor, so a draw-on grows them in.

`dash > 0` makes the stroke a dash pattern: `stroke` is then `[]` and
a `"dashes"` key (absent otherwise) holds one polyline per visible dash.

`width_profile` (an#161) makes the stroke a variable-width SHAPE: an
`"outlines"` key (absent otherwise) holds one polygon per stroke or dash
([`stroke_outline()`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.stroke_outline)), which the runtime fills instead of stroking.

`head_length > 0` turns the arrowhead on. While the visible length is
shorter than the head, the head is scaled by `visible / head_length` so
a draw-on grows it in rather than popping a full-size triangle at frame
one. The stroke stops [`HEAD_STROKE_INSET`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.HEAD_STROKE_INSET) of the head’s length back
from the tip, inside the head. Mirror of `runtime.js::pathGeometry`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> path_geometry([(0.0, 0.0), (10.0, 0.0)], 0.3, 0.3)
{'stroke': [], 'head': None}
```

### an.stage.path_geometry.point_at(points, cum, s)

The point at arc length `s`. Mirror of `runtime.js::pathPointAt`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.stage.path_geometry.profile_width(profile, width, u)

The stroke width at fraction `u` of the WHOLE path’s length (an#161):
`width` times the profile’s factor, linear between its stops.
Mirror of `runtime.js::pathProfileWidth`.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> profile_width([(0.0, 1.0), (1.0, 0.0)], 8.0, 0.25)
6.0
```

### an.stage.path_geometry.stroke_outline(line, start, total, width, profile)

The filled polygon of a variable-width stroke along `line` (an#161).

`line` is a trimmed piece of the path beginning at arc length `start`
of a path `total` long, so a point’s width is read at its place on the
WHOLE path: trimming never makes the width crawl. Each vertex is offset
both ways along the bisector of its legs’ normals by half its width,
lengthened to keep the stroke’s width through the corner (capped at
[`OUTLINE_MITER_LIMIT`](_autosummary/an.stage.path_geometry.html.md#an.stage.path_geometry.OUTLINE_MITER_LIMIT)). Butt ends. Left side forward, then the
right side back. Mirror of `runtime.js::pathOutline`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

```pycon
>>> stroke_outline([(0.0, 0.0), (10.0, 0.0)], 0.0, 10.0, 4.0, [(0.0, 1.0), (1.0, 0.5)])
[(0.0, 2.0), (10.0, 1.0), (10.0, -1.0), (0.0, -2.0)]
```

### an.stage.path_geometry.trim_polyline(points, cum, a, b)

The sub-polyline between arc lengths `a < b`: the two cut points and
every vertex strictly between them. Mirror of `runtime.js::pathTrim`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]


# _autosummary/an.stage.path_wobble.html.md

# an.stage.path_wobble

A hand-drawn wobble for stroked paths, applied at compile time (an#161).

A ruled line reads as a diagram; a line that wanders a little reads as drawn
by hand. The wobble displaces the FLATTENED polyline sideways (along its
normal) by a smooth seeded noise, before it reaches the wire, so the runtime
and its parity spec ([`an.stage.path_geometry`](_autosummary/an.stage.path_geometry.html.md#module-an.stage.path_geometry)) never learn about it:
trim, dashes and arrowheads work on the wobbly polyline exactly as on any
other.

- **Deterministic everywhere.** The noise is value noise over knots one
  wavelength apart, each knot’s value read from `sha256(seed:k)`, joined by
  a cubic smoothstep: only `+ - * /`, `floor` and `sqrt`, no
  trigonometry, so the compiled points are the same bytes on every machine.
- **Seeded by the entity** (`"<entity id>:<wobble_seed>"`), like a
  character’s blink phase: two arrows sharing one document wobble
  differently, and the same arrow wobbles the same way every render.
- **The ends stay put.** The amplitude ramps up over the first and down over
  the last wavelength, so a route still starts and ends where it was drawn
  to, and an arrowhead’s tip lands on its target.
- **Static.** One displacement per path. A *boil* (a wobble that changes on
  twos) is several displaced polylines on a step channel; not built.

```pycon
>>> pts = wobble_polyline([(0.0, 0.0), (200.0, 0.0)], amplitude=4.0, wavelength=50.0, seed="route:0")
>>> pts[0], pts[-1]  # the ends are where they were
((0.0, 0.0), (200.0, 0.0))
>>> 0.0 < max(abs(y) for _, y in pts) <= 4.0
True
>>> pts == wobble_polyline([(0.0, 0.0), (200.0, 0.0)], amplitude=4.0, wavelength=50.0, seed="route:0")
True
```

### Module Attributes

| [`WOBBLE_SAMPLES_PER_WAVELENGTH`](_autosummary/an.stage.path_wobble.html.md#an.stage.path_wobble.WOBBLE_SAMPLES_PER_WAVELENGTH)   | enough that the wobble reads as a curve, not a zig-zag, at any amplitude a stroke should take.   |
|----------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
| [`MAX_WOBBLE_POINTS`](_autosummary/an.stage.path_wobble.html.md#an.stage.path_wobble.MAX_WOBBLE_POINTS)               | The most points a wobbled path may carry.                                                        |

### Functions

| [`wobble_polyline`](_autosummary/an.stage.path_wobble.html.md#an.stage.path_wobble.wobble_polyline)(points, \*, amplitude, ...)      | `points` resampled and wobbled; see [`wobble_with_vertices()`](_autosummary/an.stage.path_wobble.html.md#an.stage.path_wobble.wobble_with_vertices).                            |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
| [`wobble_with_vertices`](_autosummary/an.stage.path_wobble.html.md#an.stage.path_wobble.wobble_with_vertices)(points, \*, amplitude, ...) | `points` resampled every `wavelength / 8` px and each sample moved up to `amplitude` px along the path's normal by seeded smooth noise. |
| [`wobble_point_count`](_autosummary/an.stage.path_wobble.html.md#an.stage.path_wobble.wobble_point_count)(length, wavelength)           | How many samples a wobble of `wavelength` puts along `length` px.                                                                       |

### an.stage.path_wobble.MAX_WOBBLE_POINTS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 20000*

The most points a wobbled path may carry. Every point is redrawn each
frame a trim or dash moves; past this the wavelength is too short for the
path’s length (a hair-fine wobble over a long route).

### an.stage.path_wobble.WOBBLE_SAMPLES_PER_WAVELENGTH *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 8*

enough that the wobble reads as a
curve, not a zig-zag, at any amplitude a stroke should take.

* **Type:**
  Samples per wavelength of the noise

### an.stage.path_wobble.wobble_point_count(length, wavelength)

How many samples a wobble of `wavelength` puts along `length` px.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### an.stage.path_wobble.wobble_polyline(points, , amplitude, wavelength, seed)

`points` resampled and wobbled; see [`wobble_with_vertices()`](_autosummary/an.stage.path_wobble.html.md#an.stage.path_wobble.wobble_with_vertices).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### an.stage.path_wobble.wobble_with_vertices(points, , amplitude, wavelength, seed)

`points` resampled every `wavelength / 8` px and each sample moved
up to `amplitude` px along the path’s normal by seeded smooth noise.

The original vertices are kept as samples (a corner stays a corner) and
move along the bisector of their two legs. Raises `ValueError` when the
result would carry more than [`MAX_WOBBLE_POINTS`](_autosummary/an.stage.path_wobble.html.md#an.stage.path_wobble.MAX_WOBBLE_POINTS) points.

Also returns, for each input point, its index in the output (where a
vertex landed), so a caller can find the authored points on the wobbled
line ([`an.stage.paths.drawn_polyline()`](_autosummary/an.stage.paths.html.md#an.stage.paths.drawn_polyline)).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]]


# _autosummary/an.stage.paths.html.md

# an.stage.paths

Stroked paths: routes, invasion arrows, borders, timelines, connectors.

One drawable covers the whole map-and-infographic motif family (epic #9,
Wave 9; an#160): a stroke along a polyline or a chain of cubic Béziers, with an
animatable **trim** — the visible span, as fractions of arc length — and an
optional **arrowhead** that rides the moving tip, oriented along the path.

```pycon
>>> arrow = PathDescriptor(
...     name="route",
...     points=[(-300.0, 0.0), (0.0, 0.0), (0.0, 200.0)],
...     arrowhead=True,
... )
>>> arrow.kind, arrow.curve, arrow.trim_end
('PathDescriptor', 'polyline', 1.0)
>>> arrow.head_length_px, arrow.head_width_px  # defaults scale with the stroke
(28.0, 24.0)
```

**Where it lives, and why there.** A path is a prop — a drawable that is not a
person — so its document sits in the `props` store beside
[`an.stage.props.PropDescriptor`](_autosummary/an.stage.props.html.md#an.stage.props.PropDescriptor) and a scene names it with an ordinary
`AssetRef(kind="prop", ...)`. The compiler dispatches on the document’s
`kind`. That keeps the scene IR unchanged (no field, no migration) and gives
a path stage placement (`at`, `scale`) and entity ordering for free.

Geometry is often per-shot (the same arrow style, a different route), so an
entity’s `overrides` are merged over the stored document and the result is
validated **strictly** — [`resolve_path()`](_autosummary/an.stage.paths.html.md#an.stage.paths.resolve_path) is the one place that happens,
and both the compiler and `an validate` call it, so their verdicts agree.

**Trim is an ordinary property.** `trim_start` / `trim_end` are numeric
node properties in `an.base.TRANSFORM_PROPERTIES`, animated by ordinary
`set`/`tween` actions and flattened to the canonical timeline like
`alpha`: `tween route trim_end 0 -> 1` is a draw-on. The fields below are
only the values the path shows before anything animates it.

Why a separate document rather than a `PropDescriptor` field: a
`PropDescriptor` is a *rig* (bones, slots, skins, swap sets) whose art is
SVG; a path has none of those, and its colour is decided by the compiler
(which is what will let a [`an.styles.StylePack`](_autosummary/an.styles.html.md#an.styles.StylePack) reach it), not by art.

### Module Attributes

| [`PATH_DOCUMENT_KIND`](_autosummary/an.stage.paths.html.md#an.stage.paths.PATH_DOCUMENT_KIND)   | Its own versioned document kind, registered from the module that owns the schema — the rule `PropDescriptor` and `CharacterDescriptor` follow.   |
|-----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DFLT_STROKE_COLOUR`](_autosummary/an.stage.paths.html.md#an.stage.paths.DFLT_STROKE_COLOUR)   | The stroke colour when the document names none.                                                                                                  |
| [`MIN_DASH_PERIOD`](_autosummary/an.stage.paths.html.md#an.stage.paths.MIN_DASH_PERIOD)      | The shortest dash period (dash + gap), scene pixels.                                                                                             |

### Functions

| [`resolve_path`](_autosummary/an.stage.paths.html.md#an.stage.paths.resolve_path)(document[, overrides])            | The path an entity draws: its stored document with `overrides` on top.                                                                                                                                                                                                                                                                                     |
|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`drawn_polyline`](_autosummary/an.stage.paths.html.md#an.stage.paths.drawn_polyline)(desc, entity_id)                | The polyline the compiler puts on the wire for entity `entity_id` drawing `desc` — flattened, closed, wobbled (the wobble is seeded by the entity) — and the index in it of each AUTHORED on-path point: every point of a polyline, `p0 p1 p2 ...` of a cubic chain (not its controls), and the closing return to the first point when `closed` added one. |
| [`draw_on_through`](_autosummary/an.stage.paths.html.md#an.stage.paths.draw_on_through)(entity_id, path, arrivals, \*) | A draw-on whose tip reaches each authored point at its own time (an#161).                                                                                                                                                                                                                                                                                  |

### Classes

| [`PathDescriptor`](_autosummary/an.stage.paths.html.md#an.stage.paths.PathDescriptor)(\*\*data)   | The on-disk path schema, saved as a prop's `prop.json`.   |
|-----------------------------------------------------------------------------|-----------------------------------------------------------|

### an.stage.paths.DFLT_STROKE_COLOUR *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '#c0392b'*

The stroke colour when the document names none. A `StylePack`’s `stroke`
role replaces it (an#161) — but only this default: a document that sets
`color` itself is art, not a default, and a pack does not rewrite art (the
same line `an.styles` draws for SVG). A per-entity `stroke` override in the
pack wins over both.

### an.stage.paths.MIN_DASH_PERIOD *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 1.0*

The shortest dash period (dash + gap), scene pixels. Bounds the number of
dashes a path can ask the runtime to redraw every frame: a path a few
thousand pixels long is a few thousand dashes at most.

### an.stage.paths.PATH_DOCUMENT_KIND *: [DocumentKind](_autosummary/an.ir.html.md#an.ir.DocumentKind)* *= DocumentKind(name='PathDescriptor', version_field='schema_version', current_version='0.1.0')*

Its own versioned document kind, registered from the module that owns the
schema — the rule `PropDescriptor` and `CharacterDescriptor` follow.

### *class* an.stage.paths.PathDescriptor(\*\*data)

Bases: `BaseModel`

The on-disk path schema, saved as a prop’s `prop.json`.

`extra="forbid"`, unlike the store documents around it: a path is a
precise drawing instruction, and a misspelt `trim_ends` that silently
did nothing is the defect class this package refuses (the `Plane`
precedent, an#110).

`curve="cubic"` reads `points` as chained cubic Béziers,
`p0 c1 c2 p1 c1 c2 p2 ...` — `3n + 1` points, the SVG `C` command
chained:

```pycon
>>> PathDescriptor(name="s", curve="cubic", points=[(0, 0), (1, 1), (2, 1), (3, 0)]).curve
'cubic'
>>> PathDescriptor(name="s", curve="cubic", points=[(0, 0), (1, 1), (3, 0)])
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for PathDescriptor
  Value error, a cubic path takes 3n + 1 points (p0, then c1 c2 p per segment); got 3 [type=value_error, input_value=..., input_type=dict]
...
```

#### closed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

the path returns to its first point (a straight
closing leg is added when the last point is elsewhere) and the stroke
joins there instead of ending in two caps. Trim still runs from the
first point round to it again.

* **Type:**
  A closed shape (an#161)

#### color *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`#rrggbb`.

#### dash *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`dash` on, `gap` off, repeating along
the path from ITS start — anchored to the path, not to the trimmed span,
so a draw-on reveals dashes in place instead of making them crawl.
`gap` defaults to `dash`. `None` = a solid stroke.

* **Type:**
  A dash pattern, scene pixels

#### dash_offset *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Shifts the pattern along the path (positive = forward). An ordinary
numeric node property like `trim_end`, so `tween route dash_offset`
is the “marching ants” route; only a dashed path has one.

#### fill *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The region a closed path encloses, `#rrggbb`; `None` = unfilled.
Drawn under the stroke and NOT trimmed: a draw-on draws the border and
the fill is there throughout. To fade a region in separately, make it
its own entity (`width: 0`, filled) and tween that node’s `alpha`.

#### fill_alpha *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The fill’s opacity, `0..1`.

#### *property* gap_px *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The gap of the dash pattern, scene pixels (`dash` when unset).

#### head_length *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Scene pixels; `None` = a multiple of `width`.

#### *property* head_length_px *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The arrowhead’s length in scene pixels.

#### *property* head_width_px *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The arrowhead’s base width in scene pixels.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### points *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]]*

Scene pixels, relative to the node’s origin (`AssetRef.stage.at`).

#### sampling *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['parameter', 'arclength']*

`"parameter"` (uniform in
the curve’s parameter, the default) or `"arclength"` (evenly along it).

* **Type:**
  How a cubic’s samples are spaced (an#161)

#### tail_arrowhead *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

An arrowhead at the START too, pointing back along the path (an#161):
with `arrowhead`, a double-headed arrow. Same size as the end’s.

#### trim_start *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The visible span before anything animates it, as fractions of arc
length. `trim_end=0` starts a draw-on hidden, and a trim tween
with no `from_value` starts from these values (not the global rest).

#### width *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Stroke width, scene px. `0` = no stroke, only for a 

```
``
```

fill\`\`ed shape
(a region without a border).

#### width_profile *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`[[t, factor], ...]` along the WHOLE path’s
arc length (`t` from 0 to 1, increasing; `factor` times `width`,
linear between stops), so `[[0, 1], [1, 0]]` tapers to a point and a
trim never makes the width crawl. The stroke becomes a filled shape:
butt ends, mitred corners (no `cap`/`join`).

* **Type:**
  A variable width (an#161)

#### wobble *: [float](https://docs.python.org/3/builtins/functions.html#float)*

the stroke wanders up to this many scene
px either side of its line, by seeded smooth noise applied at compile
([`an.stage.path_wobble`](_autosummary/an.stage.path_wobble.html.md#module-an.stage.path_wobble)), its ends left where they are. `0` = a
ruled line.

* **Type:**
  A hand-drawn wobble (an#161)

#### wobble_seed *: [int](https://docs.python.org/3/builtins/functions.html#int)*

the noise is seeded by the entity’s id
and this number.

* **Type:**
  Another wobble of the same path

#### wobble_wavelength *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The wobble’s wavelength, scene px; `None` = a multiple of `width`.

#### *property* wobble_wavelength_px *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The wobble’s wavelength in scene pixels.

### an.stage.paths.draw_on_through(entity_id, path, arrivals, , start=0.0, easing='ease_in_out')

A draw-on whose tip reaches each authored point at its own time (an#161).

`arrivals[k]` is when the tip reaches authored point `k + 1` (the tip
is at point 0, hidden, at `start`): a route that reaches each city on a
beat, or slows into the last turn. One `tween` of `trim_end` per leg,
from the arc fraction of one point to the next on the polyline the
compiler draws ([`drawn_polyline()`](_autosummary/an.stage.paths.html.md#an.stage.paths.drawn_polyline), wobble included, so the tip is ON
the point), each eased by `easing`, preceded by a `set` of
`trim_end` to 0 at 0. `arrivals` are absolute shot times, increasing.

Returns a list of top-level actions for `shot.actions.extend(...)`, as
[`an.stage.text.reveal_units()`](_autosummary/an.stage.text.html.md#an.stage.text.reveal_units) does.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

```pycon
>>> acts = draw_on_through("r", {"kind": "PathDescriptor", "name": "r",
...     "points": [[0, 0], [30, 0], [30, 10]]}, [1.0, 3.0])
>>> [(a.kind, getattr(a, "to_value", getattr(a, "value", None))) for a in acts]
[('set', 0.0), ('tween', 0.75), ('sequence', None)]
```

### an.stage.paths.drawn_polyline(desc, entity_id)

The polyline the compiler puts on the wire for entity `entity_id`
drawing `desc` — flattened, closed, wobbled (the wobble is seeded by
the entity) — and the index in it of each AUTHORED on-path point: every
point of a polyline, `p0 p1 p2 ...` of a cubic chain (not its controls),
and the closing return to the first point when `closed` added one.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]]

```pycon
>>> pts, anchors = drawn_polyline(PathDescriptor(name="r", points=[(0, 0), (10, 0), (10, 10)], closed=True), "r")
>>> pts[anchors[-1]], anchors
((0.0, 0.0), [0, 1, 2, 3])
```

### an.stage.paths.resolve_path(document, overrides=None)

The path an entity draws: its stored document with `overrides` on top.

Validated strictly after the merge, so an override key the schema does not
know raises instead of vanishing (an environment override silently drops
unknown keys; a path’s does not).

* **Return type:**
  [`PathDescriptor`](_autosummary/an.stage.paths.html.md#an.stage.paths.PathDescriptor)

```pycon
>>> doc = {"kind": "PathDescriptor", "name": "a", "points": [[0, 0], [10, 0]]}
>>> resolve_path(doc, {"points": [[0, 0], [0, 50]]}).points
[(0.0, 0.0), (0.0, 50.0)]
>>> try:
...     resolve_path(doc, {"colour": "#000000"})
... except ValueError as e:
...     print(str(e).split("Value error, ")[1].split(". The fields")[0])
unknown PathDescriptor field(s): 'colour' (did you mean 'color', 'curve', 'source'?)
```


# _autosummary/an.stage.preview.html.md

# an.stage.preview

Live preview server: render a project’s current scene in a browser, reloading on edit.

The [`preview_project()`](_autosummary/an.stage.preview.html.md#an.stage.preview.preview_project) function spins up a local HTTP server pointed at
a freshly-compiled cutout scene JSON, then watches `scene.md` /
`ir/scene.json` for changes and recompiles. The browser polls
`scene.json` every ~500 ms (HEAD request, `Last-Modified` header) and
re-calls `window.anLoadScene` whenever the file changes.

Lossy by design: shows the runtime canvas only — no audio mux, no final
mp4. Use `an render` once you’re happy with the look.

```pycon
>>> from an.stage.preview import _stage_preview
>>> callable(_stage_preview)
True
```

### Functions

| [`preview_project`](_autosummary/an.stage.preview.html.md#an.stage.preview.preview_project)(project_dir, \*[, shot_id, ...])   | Serve a live preview of `project_dir` from a local HTTP server.   |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|

### Classes

| [`PreviewStaging`](_autosummary/an.stage.preview.html.md#an.stage.preview.PreviewStaging)(runtime_dir, scene_json_path, ...)   | Outcome of staging a preview's runtime + initial compiled scene.   |
|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|

### Exceptions

| [`PreviewError`](_autosummary/an.stage.preview.html.md#an.stage.preview.PreviewError)   | Raised when a preview cannot be staged or served.   |
|-----------------------------------------------------------------|-----------------------------------------------------|

### *exception* an.stage.preview.PreviewError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a preview cannot be staged or served.

### *class* an.stage.preview.PreviewStaging(runtime_dir, scene_json_path, shot_id)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of staging a preview’s runtime + initial compiled scene.

### an.stage.preview.preview_project(project_dir, , shot_id=None, open_browser=True, poll_interval_s=0.5)

Serve a live preview of `project_dir` from a local HTTP server.

Compiles the chosen shot (default: first shot in the timeline) to its
runtime JSON, stages the cutout JS runtime + any SVG character
textures, and serves the result on a free port. A daemon watcher
thread polls `scene.md` / `ir/scene.json` mtimes and recompiles
when either changes; the browser sees the new compiled scene via its
own ~500 ms HEAD-request poll.

Blocks the calling thread until interrupted (Ctrl-C). Returns the
base URL after teardown.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.stage.prop_validate.html.md

# an.stage.prop_validate

Check a prop’s asset folder offline, and say what a prop must be (an#340).

`an props validate <dir>` and `an props contract`: the prop’s twin of
`an character validate` / `an character contract`, in the core because a
prop is the stage’s own rig (`an.stage.props.PropDescriptor`), not a genre’s.
Before it, a carved prop’s layout rules lived in its `prop.json` metadata and
nothing read them.

The rig rules are the stage’s ([`an.stage.rig`](_autosummary/an.stage.rig.html.md#module-an.stage.rig)), so this validator, `an
validate` and the rig builder say the same thing: a structural problem
([`rig_problems()`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_problems)) blocks; where the stage cannot draw a
nested chain ([`chain_draw_order_problems()`](_autosummary/an.stage.rig.html.md#an.stage.rig.chain_draw_order_problems)), the origin and
the rest pose are advisories here, because they are the stage’s limits or a
likely slip, not a malformed rig.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     report = validate_prop(d, name="nothing")
>>> report.passed, report.findings[0].description
(False, "nothing has no prop.json, or it is not a PropDescriptor (kind 'PropDescriptor')")
```

### Module Attributes

| [`PROP_META_NAME`](_autosummary/an.stage.prop_validate.html.md#an.stage.prop_validate.PROP_META_NAME)   | The descriptor file a prop folder holds (the props store's sidecar name).   |
|-------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`BLOCKING`](_autosummary/an.stage.prop_validate.html.md#an.stage.prop_validate.BLOCKING)         | Finding severities, the ones `an character validate` uses.                  |

### Functions

| [`render_prop_contract`](_autosummary/an.stage.prop_validate.html.md#an.stage.prop_validate.render_prop_contract)()              | What a prop folder must hold, read off the schema and the rig rules (never retyped).   |
|--------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|
| [`validate_prop`](_autosummary/an.stage.prop_validate.html.md#an.stage.prop_validate.validate_prop)(prop_dir, \*[, name]) | Check a prop folder (`prop.json` + `parts/`) against the rig contract, offline.        |

### an.stage.prop_validate.BLOCKING *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'error'*

Finding severities, the ones `an character validate` uses.

### an.stage.prop_validate.PROP_META_NAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'prop.json'*

The descriptor file a prop folder holds (the props store’s sidecar name).

### an.stage.prop_validate.render_prop_contract()

What a prop folder must hold, read off the schema and the rig rules (never retyped).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> "nesting" in render_prop_contract()
True
```

### an.stage.prop_validate.validate_prop(prop_dir, , name=None)

Check a prop folder (`prop.json` + `parts/`) against the rig contract, offline.

Blocking: no or an unreadable descriptor, a structural rig problem (an
unknown bone, a bone cycle), an attachment whose art is not in the folder,
a prop that draws nothing. Advisory: a chain the stage cannot draw in its
declared order (the compiler refuses it), the origin, the rest pose, an
unpopulated `AssetSource`.

* **Return type:**
  [`VerificationReport`](_autosummary/an.verify.html.md#an.verify.VerificationReport)


# _autosummary/an.stage.props.html.md

# an.stage.props

Props: a rig whose art is not a person.

A lamp, a sword, a sign. Structurally a prop is what a character already is —
bones, slots, skins, attachments, swap sets — and the cutout compiler builds
both with the **same** rig builder, which is why an#108 made the art store an
argument rather than writing a second one.

What differs is the *descriptor*, and the difference is entirely in the
defaults:

| `CharacterDescriptor`        | `PropDescriptor`   |
|------------------------------|--------------------|
| seven-bone humanoid          | one `root` bone    |
| face, eyes, brows, mouth     | one `body` slot    |
| `idle_breath` + `blink`      | no animations      |
| viseme + eyelid `asset_sets` | none               |
| `face_overlay` matters       | no face at all     |

\*\*Why not `CharacterDescriptor` with `kind: "prop"`.\*\* Three measured reasons,
each of which turns a one-field change into a silent wrong render:

1. `CharacterDescriptor.model_post_init` re-seeds the humanoid skeleton, the
   face slots, the default skin **and** `idle_breath`/`blink` from an empty
   list — so `CharacterDescriptor(name="sword")` is a seven-bone person with a
   blinking face, not an empty rig.
2. The compiler’s placeholder fallback draws a **person** where a lamp should
   be (the an#33 failure mode), so a prop whose art fails to resolve renders
   as a humanoid rather than as nothing.
3. `an character validate` scores a correct prop at **22** blocking findings —
   `REQUIRED_PARTS` (12) plus `MOUTH_SHAPES` (9), none of which a lamp has,
   plus “has no character.json”, which is the tool saying out loud that it was
   handed the wrong kind of document. (Measured on
   `tests/fixtures/props/lamp/`; an earlier draft of this docstring said 21,
   having added the two lists without running the tool.)

\*\*Why not a new minimal document with `states`.\*\* That is `asset_sets`
renamed: it would need a rename table at the compiler boundary and would cap a
prop at one moving piece. A prop reuses the swap-channel machinery instead, so
a two-state lamp is `asset_sets={"lamp": {"off": ..., "on": ...}}` and
`set lamp on` in the scene — the same words a character’s viseme swap uses.

### Module Attributes

| [`PROP_SCHEMA_VERSION`](_autosummary/an.stage.props.html.md#an.stage.props.PROP_SCHEMA_VERSION)   | the bones' rest pose (`rotation_deg`, `scale_*`) poses the built parts; the migration protects a 0.1.0 rig that carried one.                                                                                                                                                |
|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`PROP_DOCUMENT_KIND`](_autosummary/an.stage.props.html.md#an.stage.props.PROP_DOCUMENT_KIND)    | Its own versioned document, registered from the module that owns the schema — the same rule `CharacterDescriptor` follows, and the reason the migration registry is keyed per KIND: two documents at `0.1.0` that migrate differently is exactly the collision an#77 fixed. |

### Functions

| [`default_prop_bones`](_autosummary/an.stage.props.html.md#an.stage.props.default_prop_bones)([view_box])   | One bone, at the CENTRE of the view_box (an#408).   |
|-----------------------------------------------------------------------------------|-----------------------------------------------------|
| [`default_prop_slots`](_autosummary/an.stage.props.html.md#an.stage.props.default_prop_slots)()             | One slot on that bone.                              |

### Classes

| [`PropDescriptor`](_autosummary/an.stage.props.html.md#an.stage.props.PropDescriptor)(\*\*data)   | The on-disk prop schema.   |
|-----------------------------------------------------------------------------|----------------------------|

### an.stage.props.PROP_DOCUMENT_KIND *: [DocumentKind](_autosummary/an.ir.html.md#an.ir.DocumentKind)* *= DocumentKind(name='PropDescriptor', version_field='schema_version', current_version='0.2.0')*

Its own versioned document, registered from the module that owns the schema
— the same rule `CharacterDescriptor` follows, and the reason the migration
registry is keyed per KIND: two documents at `0.1.0` that migrate
differently is exactly the collision an#77 fixed.

### an.stage.props.PROP_SCHEMA_VERSION *= '0.2.0'*

the bones’ rest pose (`rotation_deg`, `scale_*`) poses
the built parts; the migration protects a 0.1.0 rig that carried one.

* **Type:**
  0.2.0 (an#339)

### *class* an.stage.props.PropDescriptor(\*\*data)

Bases: [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument)

The on-disk prop schema. Saved as `prop.json`.

```pycon
>>> p = PropDescriptor(name="lamp")
>>> p.kind
'PropDescriptor'
>>> [b.name for b in p.bones], [s.name for s in p.slots]
(['root'], ['body'])
```

A prop is **not** seeded with a face, a skeleton or an idle animation —
the three things `CharacterDescriptor` fills in from an empty list:

```pycon
>>> p.animations, p.asset_sets, p.skins
({}, {}, {})
```

Two states are the swap-channel machinery a character’s viseme already
uses, not a second vocabulary:

```pycon
>>> lamp = PropDescriptor(
...     name="lamp",
...     skins={"default": Skin(slots={"body": {
...         "off": Attachment(path="parts/off.svg"),
...         "on": Attachment(path="parts/on.svg"),
...     }})},
...     asset_sets={"lamp": {"off": "off", "on": "on"}},
... )
>>> sorted(lamp.asset_sets["lamp"])
['off', 'on']
>>> back = PropDescriptor.model_validate_json(lamp.model_dump_json())
>>> back.skins["default"].slots["body"]["on"].path
'parts/on.svg'
```

A prop stands where it is put by its declared `origin` (an#338), in
view_box units; unset, by the centre of its bones’ extent, and the stored
document does not mention it:

```pycon
>>> PropDescriptor(name="tripod", origin=(512, 1010)).origin
(512.0, 1010.0)
>>> "origin" in PropDescriptor(name="lamp").model_dump()
False
```

#### animations *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

Present so `play` and the rig builder read the same attribute on either
descriptor. Empty by default — a prop has no `idle_breath` and no
`blink`, and seeding one would animate a lamp.

#### asset_sets *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]]*

`{channel: {key: attachment_name}}` — the same indirection a character
uses for visemes. Empty by default: a prop with no moving parts declares
none, and declaring a channel a rig cannot serve is what makes a swap
silently keep the previous texture.

#### face_overlay *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)[True]*

Always true, and not a knob. `face_overlay=False` means “the face is
baked into the head art”, which makes the builder suppress every slot
nested under the head bone’s primary slot. A prop has no head bone, so
the flag can only do harm; it exists because the shared builder reads it.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### model_post_init(\_PropDescriptor_\_context)

Override this method to perform additional initialization after `__init__` and `model_construct`.
This is useful if you want to do some validation that requires the entire model to be initialized.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### source *: [AssetSource](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Where this art came from and what its licence obliges. `None` means
“we made this” — not “unknown”. Same field as `CharacterDescriptor`,
because `an credits` should not need to know which store it came from.

#### source_svg *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Optional source SVG the `parts/` folder was sliced from.

### an.stage.props.default_prop_bones(view_box=(0, 0, 1024, 1024))

One bone, at the CENTRE of the view_box (an#408).

A part is centred on its bone (anchor 0.5), so with the bone at the centre
a full-canvas part’s art coordinates ARE the rig’s: an `origin` read off
the drawing (a foot at `(512, 1000)` in a 1024 box) lands where it says.
Before an#408 the bone sat at `(0, 0)` and an origin had to be measured
from the art’s centre. Nothing moves for a prop without an `origin`: it
is placed by its bones’ extent centre, which is this bone wherever it is.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Bone`](_autosummary/an.stage.rig.html.md#an.stage.rig.Bone)]

```pycon
>>> [(b.name, b.x, b.y) for b in default_prop_bones()]
[('root', 512.0, 512.0)]
```

### an.stage.props.default_prop_slots()

One slot on that bone.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Slot`](_autosummary/an.stage.rig.html.md#an.stage.rig.Slot)]

```pycon
>>> [(s.name, s.bone, s.draw_order) for s in default_prop_slots()]
[('body', 'root', 0)]
```


# _autosummary/an.stage.raster.html.md

# an.stage.raster

Raster art: what a PNG, JPEG or WebP is, read from its header (an#211).

Every piece of art the cutout renderer drew was SVG, and the size probe said
so: `svg_raster_size` parses its input as XML, so a PNG
plate died in the compiler with `ParseError: not well-formed (invalid token):
line 1, column 0` — the error a user sees for “I gave it a picture”. Art carved
out of footage, a scanned drawing, a photographed paper cut-out: those are
raster, and tracing them to SVG throws away the shading that made them worth
carving.

PixiJS loads PNG, JPEG and WebP natively (its `loadTextures` parser, chosen by
the file’s extension), so the renderer needed nothing new. What the COMPILER
needed was three answers it previously got only from an SVG:

- **how big is it** — [`image_size()`](_autosummary/an.stage.raster.html.md#an.stage.raster.image_size), read from the header (no decode, no
  dependency: the four formats state their pixel size in the first few dozen
  bytes, and a header parse is the same cost as the SVG probe it sits beside);
- **what size does this art draw at, whatever it is** — [`art_size()`](_autosummary/an.stage.raster.html.md#an.stage.raster.art_size), the
  one probe the compiler, the fidelity check and `an validate` call;
- **what exactly is it** — [`content_digest()`](_autosummary/an.stage.raster.html.md#an.stage.raster.content_digest), because a raster texture is
  addressed by its bytes: a re-carved part is a different texture (the runtime’s
  loader ignores a re-added alias on hot reload, an#155) and a different
  compiled contract, so the contract hash covers the pixels it will draw.

**EXIF orientation** (an#218). A JPEG whose EXIF says “rotate 90°” (a phone
photo) is decoded upright by Chromium, so its box is the stored size
transposed: [`image_size()`](_autosummary/an.stage.raster.html.md#an.stage.raster.image_size) reads the orientation tag and swaps width and
height for the four orientations that turn the picture a quarter (5-8).

**Why a raster part is never recoloured.** A StylePack recolours SVG art by
rewriting the literal colours its descriptor tags (`colour_roles`). A raster
has no literals — its colours are pixels, and inferring a role from a pixel is
exactly what caused an#99’s wrong-tone lid — so it renders as drawn, and the
compiler says so once.

```pycon
>>> is_raster("parts/head.png"), is_raster("parts/head.svg")
(True, False)
```

### Module Attributes

| [`RASTER_SUFFIXES`](_autosummary/an.stage.raster.html.md#an.stage.raster.RASTER_SUFFIXES)   | The raster formats a plate or a part may be, by file suffix.   |
|--------------------------------------------------------------------|----------------------------------------------------------------|

### Functions

| [`art_size`](_autosummary/an.stage.raster.html.md#an.stage.raster.art_size)(source)           | The size a piece of art rasterises at, whatever format it is.        |
|-----------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`content_digest`](_autosummary/an.stage.raster.html.md#an.stage.raster.content_digest)(path)       | The hex sha256 of a file's bytes, cached by (path, mtime, size).     |
| [`has_alpha`](_autosummary/an.stage.raster.html.md#an.stage.raster.has_alpha)(source)          | Whether the image can be transparent anywhere; `None` if unknown.    |
| [`image_size`](_autosummary/an.stage.raster.html.md#an.stage.raster.image_size)(source)         | `(width, height)` in pixels of a PNG, JPEG or WebP, from its header. |
| [`is_raster`](_autosummary/an.stage.raster.html.md#an.stage.raster.is_raster)(src)             | Whether `src` names raster art, by its suffix (case-insensitive).    |
| [`strip_version`](_autosummary/an.stage.raster.html.md#an.stage.raster.strip_version)(src)         | The file path a (possibly versioned) texture `src` names.            |
| [`versioned_src`](_autosummary/an.stage.raster.html.md#an.stage.raster.versioned_src)(src, digest) | `src` with its content digest as a query string.                     |

### Exceptions

| [`RasterFormatError`](_autosummary/an.stage.raster.html.md#an.stage.raster.RasterFormatError)   | A file named as raster art whose header this module cannot read.   |
|----------------------------------------------------------------------|--------------------------------------------------------------------|

### an.stage.raster.RASTER_SUFFIXES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('.png', '.jpg', '.jpeg', '.webp')*

The raster formats a plate or a part may be, by file suffix. PixiJS 7’s
`loadTextures` picks its parser by extension and accepts exactly these
(plus AVIF, left out because Chromium’s AVIF decode is the one of the four
whose output is not specified bit-exactly and the render is a contract).

### *exception* an.stage.raster.RasterFormatError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A file named as raster art whose header this module cannot read.

A `ValueError`, so the compiler’s size probe treats it exactly like a
malformed SVG: the art is declared (and fails loudly at load) rather than
silently dropped.

### an.stage.raster.art_size(source)

The size a piece of art rasterises at, whatever format it is.

SVG: its declared `width`/`height` (else its viewBox), as the browser
does — `svg_raster_size()`. Raster: its pixel size.
The ONE probe the compiler, the fidelity check and `an validate` share, so
none of them can size a PNG as if it were XML again.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.stage.raster.content_digest(path)

The hex sha256 of a file’s bytes, cached by (path, mtime, size).

The ENCODED bytes, not the pixels: re-saving the same image with another
encoder changes the digest (and so the alias and the contract hash) —
which is the conservative direction for a contract.

Cached because a rig registers every attachment of every slot and a scene
compiles each shot separately; keyed on the stat so an edited file is
re-read, which is what makes the digest safe to put in a texture alias.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.stage.raster.has_alpha(source)

Whether the image can be transparent anywhere; `None` if unknown.

A PNG says so in its header (colour type 4 or 6) or with a `tRNS` chunk;
a JPEG never can; a WebP says so in its `VP8X` flags or by being lossless
with an alpha bit. A cut-out part without alpha is a rectangle — the whole
canvas draws, background and all — which is what `an character validate`
uses this for.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.stage.raster.image_size(source)

`(width, height)` in pixels of a PNG, JPEG or WebP, from its header.

`source` is a path or the file’s bytes.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> import struct, zlib
>>> ihdr = struct.pack(">IIBBBBB", 3, 2, 8, 6, 0, 0, 0)
>>> png = (b"\x89PNG\r\n\x1a\n" + struct.pack(">I", 13) + b"IHDR" + ihdr
...        + struct.pack(">I", zlib.crc32(b"IHDR" + ihdr)))
>>> image_size(png)
(3.0, 2.0)
```

### an.stage.raster.is_raster(src)

Whether `src` names raster art, by its suffix (case-insensitive).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_raster("plates/Street.JPG")
True
>>> is_raster("data:image/svg+xml;base64,AAAA")
False
```

### an.stage.raster.strip_version(src)

The file path a (possibly versioned) texture `src` names.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> strip_version("props/lamp/parts/on.png?v=abc123")
'props/lamp/parts/on.png'
```

### an.stage.raster.versioned_src(src, digest)

`src` with its content digest as a query string.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> versioned_src("props/lamp/parts/on.png", "abc123")
'props/lamp/parts/on.png?v=abc123'
```


# _autosummary/an.stage.render.html.md

# an.stage.render

The 2D stage engine (`runtime.js` in headless Chromium), and the cut-out renderer built on it.

Since an#247 this module is an ENGINE, not a whole renderer: the frame stage is
the core’s ([`an.engines.frame_stage_renderer()`](_autosummary/an.engines.html.md#an.engines.frame_stage_renderer) – clock, capture loop,
supersample and shutter resolves, MP4 sink, provenance), and this module only
does what is specific to the stage:

1. [`StageEngine`](_autosummary/an.stage.render.html.md#an.stage.render.StageEngine) compiles the shot to a `CutoutSceneJSON`
   (`compile_shot`), stages a copy of the JS runtime plus the shot’s textures
   in `<work_dir>/shot_<id>/runtime/`, serves it over loopback HTTP (PixiJS
   cannot fetch `file://` in headless Chromium), launches Chromium with the
   pinned rasteriser flags, injects the supersample factor, loads the scene with
   a deadline, and judges the determinism probe.
2. It yields a session the core drives: `_CanvasStageSession` (the
   default, `capture="canvas"`: batches of in-page canvas reads,
   `window.anCaptureFrames`) or `_ScreenshotStageSession`
   (`capture="screenshot"`: an element screenshot per instant). Both are
   batched (`frames(requests)`), so a runtime throw is located by frame.
3. [`CutoutRenderer`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderer) is `frame_stage_renderer(StageEngine())` under the
   persisted renderer name `cutout`, raising `CutoutRenderError`.

The engine-independent halves moved to the core in an#247 and are still
reachable here by their old names: the mux, the pixel format and the x264 argv
([`an.media.mp4`](_autosummary/an.media.mp4.html.md#module-an.media.mp4)), the frame naming ([`an.media.frames`](_autosummary/an.media.frames.html.md#module-an.media.frames)), the resolves
([`an.media.supersample`](_autosummary/an.media.supersample.html.md#module-an.media.supersample), [`an.media.shutter`](_autosummary/an.media.shutter.html.md#module-an.media.shutter)) and the capture tunables
([`an.engines.capture`](_autosummary/an.engines.capture.html.md#module-an.engines.capture)). Those old names are LIVE aliases
(`an._shims`): rebinding `DETERMINISTIC_X264_ARGS` or `DEFAULT_PIX_FMT`
here rebinds the global the core reads, so the bench’s levers keep reaching the
encode. The module itself moved here from `an/adapters/cutout/render.py` in
an#247; that path is a live alias of this one (`an._shims.alias_module()`).

Failures are reported with concrete remediation: missing ffmpeg, missing
Chromium, runtime load timeout, etc. Subprocess errors are wrapped at the
facade boundary.

```pycon
>>> CutoutRenderer().name, CutoutRenderer().supported_renderers
('cutout', ('cutout', 'stage'))
```

### Module Attributes

| [`STAGE_ENGINE_NAME`](_autosummary/an.stage.render.html.md#an.stage.render.STAGE_ENGINE_NAME)             | what it is, independent of the renderer names it is registered under (`cutout`, a persisted identifier, and `stage`).                                                                                                                                                      |
|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DEFAULT_ASSET_LOAD_TIMEOUT_MS`](_autosummary/an.stage.render.html.md#an.stage.render.DEFAULT_ASSET_LOAD_TIMEOUT_MS) | Deadline for `anLoadScene`, which awaits `PIXI.Assets.load` for every declared texture.                                                                                                                                                                                    |
| [`DETERMINISTIC_CHROMIUM_ARGS`](_autosummary/an.stage.render.html.md#an.stage.render.DETERMINISTIC_CHROMIUM_ARGS)   | Chromium launch flags that pin the rasteriser (an#31, research §2).                                                                                                                                                                                                        |
| [`DEFAULT_CAPTURE`](_autosummary/an.stage.render.html.md#an.stage.render.DEFAULT_CAPTURE)               | the runtime's `anCaptureFrames` reads the canvas in-page and hands back PNG data URLs in batches (`an.stage.canvas_capture`), which writes frames whose DECODED pixels equal the screenshot path's — ~7.8x faster in the frame stage on the golden corpus, ~2.3x at 1080p. |
| [`SUPPORTED_CAPTURES`](_autosummary/an.stage.render.html.md#an.stage.render.SUPPORTED_CAPTURES)            | a typo must fail before a browser launches, not minutes into a render.                                                                                                                                                                                                     |
| [`ASSET_LOAD_TIMEOUT_MARKER`](_autosummary/an.stage.render.html.md#an.stage.render.ASSET_LOAD_TIMEOUT_MARKER)     | Sentinel the in-page deadline rejects with, so the Python side can tell a timeout apart from a load failure and say something different about each.                                                                                                                        |
| [`EXTENSIONS_FILE`](_autosummary/an.stage.render.html.md#an.stage.render.EXTENSIONS_FILE)               | The staged file genres' runtime scripts are written to (`index.html` loads it after `runtime.js`).                                                                                                                                                                         |
| [`ASSET_SRC_PREFIX_TO_STORE`](_autosummary/an.stage.render.html.md#an.stage.render.ASSET_SRC_PREFIX_TO_STORE)     | Texture `src` prefix → the mall store that resolves the rest of the path.                                                                                                                                                                                                  |
| [`CONFORMANCE_SIZE`](_autosummary/an.stage.render.html.md#an.stage.render.CONFORMANCE_SIZE)              | the read-back does not depend on it, and a small canvas loads fast.                                                                                                                                                                                                        |
| [`StageRenderer`](_autosummary/an.stage.render.html.md#an.stage.render.StageRenderer)                 | The renderer under the engine's own name (the class is one: see [`CutoutRenderer`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderer)), and its error under the same.                                                                                                            |

### Functions

| [`effective_step_hz`](_autosummary/an.stage.render.html.md#an.stage.render.effective_step_hz)(shot, ctx)   | The stepped-timing policy a shot renders under (an#89): the shot's own `step_hz` when it declares one, else the scene's (`ctx.step_hz`), else `None` — smooth.   |
|---------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`runtime_extensions`](_autosummary/an.stage.render.html.md#an.stage.render.runtime_extensions)()           | The registered genres' runtime code for the stage, as one file's text -- `""` when none is registered (the shipped file stays as it is).                         |
| [`texture_source`](_autosummary/an.stage.render.html.md#an.stage.render.texture_source)(src_rel, mall)  | Where a texture's bytes are read from: `(path, "")`, or `(None, why)`.                                                                                           |

### Classes

| [`CutoutRenderer`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderer)([engine, name, ...])   | The stage renderer: the stage engine through the core frame stage.                                                                                              |
|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`StageEngine`](_autosummary/an.stage.render.html.md#an.stage.render.StageEngine)([name])                   | The 2D stage runtime as an [`Engine`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine).                                                  |
| [`StageRenderer`](_autosummary/an.stage.render.html.md#an.stage.render.StageRenderer)                         | The renderer under the engine's own name (the class is one: see [`CutoutRenderer`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderer)), and its error under the same. |

### Exceptions

| [`CutoutAssetWarning`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutAssetWarning)   | A declared texture could not be staged into the runtime directory.   |
|-----------------------------------------------------------------------|----------------------------------------------------------------------|
| [`CutoutRenderError`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderError)    | Raised when a cutout render fails.                                   |
| [`StageRenderError`](_autosummary/an.stage.render.html.md#an.stage.render.StageRenderError)     |                                                                      |

### an.stage.render.ASSET_LOAD_TIMEOUT_MARKER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an:asset-load-timeout'*

Sentinel the in-page deadline rejects with, so the Python side can tell a
timeout apart from a load failure and say something different about each.

### an.stage.render.ASSET_SRC_PREFIX_TO_STORE *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'characters/': 'characters', 'environments/': 'environments', 'props/': 'props', 'styles/': 'styles'}*

Texture `src` prefix → the mall store that resolves the rest of the path.

A `src` reads `<prefix>/<ref>/parts/head.svg` and resolves to
`mall[store]._root/<ref>/parts/head.svg`. Only `characters/` is emitted
by the compiler today; the others are here because environments, styles and
props all route through this same staging step as they land, and the
previous hardcoded `characters/` test silently dropped everything else.

The prefix IS the store name plus a slash for all four, which is not an
accident worth relying on: the map is the contract, and a fifth kind whose
store is named differently must still work.

### an.stage.render.CONFORMANCE_SIZE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int)]* *= (64, 48)*

the
read-back does not depend on it, and a small canvas loads fast.

* **Type:**
  The page size [`StageEngine.open_document()`](_autosummary/an.stage.render.html.md#an.stage.render.StageEngine.open_document) uses when none is given

### *exception* an.stage.render.CutoutAssetWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A declared texture could not be staged into the runtime directory.

Deliberately a warning and not an error, for now: an art package that is
still being assembled is a real state, and refusing to render it would be
worse than rendering it incompletely. But it must be *audible* — the
consequence of an un-staged texture is worse than it looks and worse than
this docstring used to claim. Measured (`misc/docs/wave4_research.md` §4):
an *absent* part file crashes the render with an unwrapped minified-PixiJS
`TypeError`; a degenerate SVG hangs it indefinitely (#79); a geometry-less
part renders invisibly. `PIXI.Texture.WHITE` — the actual white rectangle —
is reached only by a zero-byte file, an empty `src`, or no `src` key.
Either way a silent skip surfaces to the user as “the animation is broken”
rather than as an error, which is what this warning exists to prevent.

### *exception* an.stage.render.CutoutRenderError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a cutout render fails. Carries actionable detail.

### *class* an.stage.render.CutoutRenderer(engine=<factory>, name='cutout', supported_renderers=('cutout', 'stage'), error=<class 'an.stage.render.CutoutRenderError'>, capture_options=<factory>, paint_orders=('container', 'global'))

Bases: [`FrameStageRenderer`](_autosummary/an.engines.frame_stage.html.md#an.engines.frame_stage.FrameStageRenderer)

The stage renderer: the stage engine through the core frame stage.

It claims both renderer names (ADR 0001 decision 9): `stage`, the
engine’s own, and `cutout`, the persisted name every existing scene
carries. Its registry name stays `cutout` – persisted too (the shot
cache keys on it) – and [`StageRenderer`](_autosummary/an.stage.render.html.md#an.stage.render.StageRenderer) is the same class.

```pycon
>>> r = CutoutRenderer()
>>> r.name
'cutout'
>>> r.supported_renderers
('cutout', 'stage')
```

#### error

alias of [`CutoutRenderError`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderError)

#### paint_orders *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('container', 'global')*

one container’s
items sorted (`applyPaintOrder`) and one list across containers
(`applyGlobalPaint`). Read as the `engine.paint_order` capability.

* **Type:**
  The paint orders the stage runtime honours (an#430)

#### supported_renderers *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('cutout', 'stage')*

The `Shot.renderer` values this renderer claims (the ONE place it names them).

### an.stage.render.DEFAULT_ASSET_LOAD_TIMEOUT_MS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 60000*

Deadline for `anLoadScene`, which awaits `PIXI.Assets.load` for every declared
texture. **A bound is required, not merely nice**: a degenerate part SVG —
`<svg/>`, malformed XML, a zero-dimension root — makes `Assets.load` never
settle, so without this the render hangs indefinitely with no error and no
output (an#79). `page.evaluate` is not subject to Playwright’s default
timeout, so the deadline is imposed inside the page instead.

The value is a policy choice, not a measurement: it needs to sit far above a
legitimate cold load of a few dozen small SVGs and far below “a human gave
up”. Raise it for a genuinely heavy art package rather than removing it.

### an.stage.render.DEFAULT_CAPTURE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'canvas'*

the
runtime’s `anCaptureFrames` reads the canvas in-page and hands back PNG data
URLs in batches (`an.stage.canvas_capture`), which writes frames
whose DECODED pixels equal the screenshot path’s — ~7.8x faster in the frame
stage on the golden corpus, ~2.3x at 1080p. `"screenshot"`: a Playwright
element screenshot of `#stage` per instant, the path every render took
before; still available (`an render --capture screenshot`).

Flipped only after the equivalence gate (`tests/test_canvas_capture_equivalence.py`)
held on the whole golden corpus on a developer machine AND the labelled Linux
rendering lane (an#189, re-run on an#192): a faster path that moved a pixel
would silently invalidate every baseline recorded before it. Read as a MODULE
GLOBAL at call time, for `DEFAULT_PIX_FMT`’s reason — a default argument
would bind it at def time.

* **Type:**
  How frames leave the browser. `"canvas"` (the default since an#192)

### an.stage.render.DETERMINISTIC_CHROMIUM_ARGS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('--no-sandbox', '--disable-gpu', '--enable-unsafe-swiftshader', '--force-color-profile=srgb')*

Chromium launch flags that pin the rasteriser (an#31, research §2).

**Unconditional, deliberately** — not gated behind an env var. A render whose
rasteriser depends on `AN_DETERMINISTIC` is non-reproducible *by default*,
which is the property this work exists to remove; and the flags are a
measured no-op on today’s output (0 differing pixels over both fixtures,
verified on this repo at the commit that introduced them), so there is no
baseline to protect by making them opt-in.

Unpinned, the same code renders differently in ways nobody would attribute
correctly: GPU vs software rasterisation is a 1.9% / max-57 pixel difference,
and a headed browser (reachable by a one-word local edit) differs by 1.91%.
A band that wide hides any real regression.

Two flags are deliberately NOT here. `--use-angle=swiftshader` — including
Chromium’s own documented `--use-gl=angle --use-angle=swiftshader` form —
moves 1.55% of pixels by up to 58/255, so it would re-baseline the corpus for
nothing. `--disable-frame-rate-limit` measured 1.05x on this WebGL runtime
(the widely-cited 2.3x is a canvas-2D artefact). `--deterministic-mode` is a
verified no-op here, because the runtime uses `autoStart:false` plus an
explicit `app.render()`.

Record the argv **verbatim** in any provenance row: all four rasteriser
configurations report the byte-identical `UNMASKED_RENDERER_WEBGL` string,
so the renderer string cannot witness this choice.

### an.stage.render.EXTENSIONS_FILE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'extensions.js'*

The staged file genres’ runtime scripts are written to (`index.html` loads
it after `runtime.js`).

### an.stage.render.STAGE_ENGINE_NAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'stage'*

what it is, independent of the renderer names it is
registered under (`cutout`, a persisted identifier, and `stage`).

* **Type:**
  The engine’s name

### an.stage.render.SUPPORTED_CAPTURES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('screenshot', 'canvas')*

a typo must
fail before a browser launches, not minutes into a render.

* **Type:**
  The capture paths `_check_capture` accepts. Not an open string

### *class* an.stage.render.StageEngine(name='stage')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The 2D stage runtime as an [`Engine`](_autosummary/an.engines.protocol.html.md#an.engines.protocol.Engine).

Stateless: every `open()` launches its own Chromium and HTTP server, so
one instance serves a parallel render.

#### check(ctx)

Refuse an unknown capture path before anything launches.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### open_document(document, , workspace, size=(64, 48), capture=None)

A session over an already COMPILED document (a mapping in the wire
shape), with no shot and no stores: what the conformance check against
the timing vectors loads ([`an.engines.conformance`](_autosummary/an.engines.conformance.html.md#module-an.engines.conformance)). A document
that is only a timeline (`timeline` + `animations`) gets an empty
scene, so its read-back is the evaluator’s alone.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[`_StageSession`]

### an.stage.render.StageRenderError

alias of [`CutoutRenderError`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderError)

### an.stage.render.StageRenderer

The renderer under the engine’s own name (the class is one: see
[`CutoutRenderer`](_autosummary/an.stage.render.html.md#an.stage.render.CutoutRenderer)), and its error under the same.

### an.stage.render.effective_step_hz(shot, ctx)

The stepped-timing policy a shot renders under (an#89): the shot’s own
`step_hz` when it declares one, else the scene’s (`ctx.step_hz`), else
`None` — smooth. The compiler stamps whatever this returns.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> from pathlib import Path
>>> ctx = RenderContext(mall={}, work_dir=Path("."), step_hz=15.0)
>>> effective_step_hz(Shot(id="s"), ctx)
15.0
>>> effective_step_hz(Shot(id="s", step_hz=10.0), ctx)
10.0
>>> effective_step_hz(Shot(id="s"), RenderContext(mall={}, work_dir=Path("."))) is None
True
```

### an.stage.render.runtime_extensions()

The registered genres’ runtime code for the stage, as one file’s text –
`""` when none is registered (the shipped file stays as it is).

Part of the shot cache’s key when non-empty: it can change pixels.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.stage.render.texture_source(src_rel, mall)

Where a texture’s bytes are read from: `(path, "")`, or `(None, why)`.

The ONE resolution of a texture `src` to a file: staging copies from it
(`_stage_scene_assets()`) and the shot cache digests it
(`an.stage.cache_key.texture_digests`), so the bytes keyed are the bytes
drawn — a change to where art is read from (the asset library’s reference
mode, ADR 0005) changes both or neither (an#316 review). `why` is one of
`"prefix"`, `"store"` (absent or in-memory) and `"missing"`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/an.stage.rig.html.md

# an.stage.rig

The rig model the stage draws: bones, slots, skins and attachments.

A *rig* is the stage’s own idea (a skeleton of bones, draw-ordered slots bound to
them, and skins mapping slots to drawable attachments); props use it as well as
characters. It lives in `an.stage` so the stage compiler can build a rig
without importing any genre; a genre’s descriptor (`cutan`’s
`CharacterDescriptor`) is composed from these types.

Two layers live here:

- **the model**: [`Bone`](_autosummary/an.stage.rig.html.md#an.stage.rig.Bone), [`Slot`](_autosummary/an.stage.rig.html.md#an.stage.rig.Slot), [`Attachment`](_autosummary/an.stage.rig.html.md#an.stage.rig.Attachment),
  [`Skin`](_autosummary/an.stage.rig.html.md#an.stage.rig.Skin), and [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument), the base of every document that IS a
  > rig (`PropDescriptor`, `CharacterDescriptor`), which carries the fields
  > about the rig as a whole (its declared [`RigDocument.origin`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument.origin));
- **the builder**: [`build_rig_subtree()`](_autosummary/an.stage.rig.html.md#an.stage.rig.build_rig_subtree), the ONE function that turns a rig
  document into a scene subtree for props and characters alike (an#108), with
  the helpers a genre needs to call it ([`part_probe()`](_autosummary/an.stage.rig.html.md#an.stage.rig.part_probe),
  [`raster_digest()`](_autosummary/an.stage.rig.html.md#an.stage.rig.raster_digest), [`art_src()`](_autosummary/an.stage.rig.html.md#an.stage.rig.art_src), [`bone_positions()`](_autosummary/an.stage.rig.html.md#an.stage.rig.bone_positions),
  [`rig_origin()`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_origin)). Public since an#338: before it, `cutan` reached the
  > builder through private names of the stage compiler, which a second genre
  > could not have done. The compiler re-exports the old private spellings.

### Module Attributes

| [`DEFAULT_VIEW_BOX`](_autosummary/an.stage.rig.html.md#an.stage.rig.DEFAULT_VIEW_BOX)             | 1024x1024 with feet near y≈980.                                                                                                                                                                                                                                                                                 |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`RIG_DOCUMENT_OPTIONAL_FIELDS`](_autosummary/an.stage.rig.html.md#an.stage.rig.RIG_DOCUMENT_OPTIONAL_FIELDS) | The fields [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument) adds, each written out of the stored document when unset ([`omit_unset_rig_fields()`](_autosummary/an.stage.rig.html.md#an.stage.rig.omit_unset_rig_fields)), so every descriptor that never set one reads back, and hashes, as it did before the field existed. |
| [`REST_POSE_SINCE`](_autosummary/an.stage.rig.html.md#an.stage.rig.REST_POSE_SINCE)              | `{rig document kind: the version from which its bones' rest pose is applied}`, filled by [`register_rest_pose_migration()`](_autosummary/an.stage.rig.html.md#an.stage.rig.register_rest_pose_migration) (an#339).                                                                                                                              |
| [`NESTINGS`](_autosummary/an.stage.rig.html.md#an.stage.rig.NESTINGS)                     | The nesting modes ([`RigDocument.nesting`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument.nesting)); unset means the first.                                                                                                                                                                                                |
| [`RIG_HIERARCHY`](_autosummary/an.stage.rig.html.md#an.stage.rig.RIG_HIERARCHY)                | The capability a nested chain affords (ADR 0002 decision 1's name, registered by the core in [`an.capabilities.subjects`](_autosummary/an.capabilities.subjects.html.md#module-an.capabilities.subjects)).                                                                                                        |
| [`SCENE_PX_PER_VIEW_BOX`](_autosummary/an.stage.rig.html.md#an.stage.rig.SCENE_PX_PER_VIEW_BOX)        | Scene-graph pixels spanned by a descriptor's full `view_box` height.                                                                                                                                                                                                                                            |
| [`CONTAIN_FIT`](_autosummary/an.stage.rig.html.md#an.stage.rig.CONTAIN_FIT)                  | The fit policy every compiled sprite carries.                                                                                                                                                                                                                                                                   |
| [`CHARACTER_ART_PREFIX`](_autosummary/an.stage.rig.html.md#an.stage.rig.CHARACTER_ART_PREFIX)         | The `assets.textures` `src` prefix a rig's art is addressed under, which is also the mall store that resolves it (`render.ASSET_SRC_PREFIX_TO_STORE`).                                                                                                                                                          |
| [`PROP_ART_PREFIX`](_autosummary/an.stage.rig.html.md#an.stage.rig.PROP_ART_PREFIX)              | The same, for props.                                                                                                                                                                                                                                                                                            |

### Functions

| [`art_src`](_autosummary/an.stage.rig.html.md#an.stage.rig.art_src)(ref, rel_path, \*[, art_prefix])           | Path used inside the runtime dir, relative to `index.html`.                                                                                                                                                                                                                                                       |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`attachment_box`](_autosummary/an.stage.rig.html.md#an.stage.rig.attachment_box)(width, height, art)                 | The box a part draws in, in view_box units: the declared size wins, the art's aspect is kept (an#220).                                                                                                                                                                                                            |
| [`bone_extent_centre`](_autosummary/an.stage.rig.html.md#an.stage.rig.bone_extent_centre)(bones)                          | The DEFAULT point in view_box space that the entity's placement refers to, when the rig declares no [`RigDocument.origin`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument.origin) ([`rig_origin()`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_origin)).                                                                          |
| [`bone_positions`](_autosummary/an.stage.rig.html.md#an.stage.rig.bone_positions)(desc)                               | Absolute `(x, y)` per bone, in view_box units.                                                                                                                                                                                                                                                                    |
| [`bones_carry_a_rest_pose`](_autosummary/an.stage.rig.html.md#an.stage.rig.bones_carry_a_rest_pose)(doc)                       | Whether any bone of a (raw or model) rig document has a non-zero `rotation_deg` or a non-unit `scale_x`/`scale_y`.                                                                                                                                                                                                |
| [`build_rig_subtree`](_autosummary/an.stage.rig.html.md#an.stage.rig.build_rig_subtree)(entity, desc_data, \*, textures) | Build the scene subtree for a rig (a prop or a character), **from its descriptor**.                                                                                                                                                                                                                               |
| [`chain_draw_order_problems`](_autosummary/an.stage.rig.html.md#an.stage.rig.chain_draw_order_problems)(desc)                    | Where a nested chain asks the STAGE for a draw order it cannot give (an#340, an#403).                                                                                                                                                                                                                             |
| [`chain_paint_order`](_autosummary/an.stage.rig.html.md#an.stage.rig.chain_paint_order)(desc, \*[, built])               | How each container of a nested chain must order what it holds (an#403).                                                                                                                                                                                                                                           |
| [`chain_pose_problems`](_autosummary/an.stage.rig.html.md#an.stage.rig.chain_pose_problems)(desc)                          | A rest pose the stage cannot apply in a nested chain (an#340).                                                                                                                                                                                                                                                    |
| [`declared_origin`](_autosummary/an.stage.rig.html.md#an.stage.rig.declared_origin)(desc)                              | The rig's DECLARED origin as two floats, or `None` when it declares none.                                                                                                                                                                                                                                         |
| [`drawn_attachment`](_autosummary/an.stage.rig.html.md#an.stage.rig.drawn_attachment)(desc, skin, slot)                 | The `(name, attachment)` a slot draws by default, or `None`.                                                                                                                                                                                                                                                      |
| [`global_paint_order`](_autosummary/an.stage.rig.html.md#an.stage.rig.global_paint_order)(desc, \*[, built])              | The node paths of a rig's slots, relative to its entity, in DECLARED draw order: the paint list the stage uses when a nested chain interleaves with an unrelated part (an#430), which no sort of a container's own items can give ([`chain_draw_order_problems()`](_autosummary/an.stage.rig.html.md#an.stage.rig.chain_draw_order_problems)). |
| [`legacy_rest_pose_unknown`](_autosummary/an.stage.rig.html.md#an.stage.rig.legacy_rest_pose_unknown)(raw, kind, \*, since)     | The builder guard (an#339): whether `raw` may be a pre-rest-pose document that its migration could not see.                                                                                                                                                                                                       |
| [`natural_paint_order`](_autosummary/an.stage.rig.html.md#an.stage.rig.natural_paint_order)(order, keys)                   | What each container holds in the builder's own order: the slot's visual first, then its children by their own `(draw_order, name)`.                                                                                                                                                                               |
| [`nesting_of`](_autosummary/an.stage.rig.html.md#an.stage.rig.nesting_of)(desc)                                   | `"flat"` or `"bones"` (an#340); unset is flat.                                                                                                                                                                                                                                                                    |
| [`omit_unset_rig_fields`](_autosummary/an.stage.rig.html.md#an.stage.rig.omit_unset_rig_fields)(data)                        | Drop every unset [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument) field from a dumped document, in place.                                                                                                                                                                                             |
| [`part_probe`](_autosummary/an.stage.rig.html.md#an.stage.rig.part_probe)(characters_store, \*[, art_prefix])     | A probe answering `(art exists, the size it rasterises at)` for a part.                                                                                                                                                                                                                                           |
| [`primary_slot_per_bone`](_autosummary/an.stage.rig.html.md#an.stage.rig.primary_slot_per_bone)(desc)                        | `{bone name: the slot that IS that bone}`, when one exists.                                                                                                                                                                                                                                                       |
| [`protect_legacy_rest_pose`](_autosummary/an.stage.rig.html.md#an.stage.rig.protect_legacy_rest_pose)(doc)                      | The migration step every rig kind runs onto the version that applies the rest pose (an#339).                                                                                                                                                                                                                      |
| [`raster_digest`](_autosummary/an.stage.rig.html.md#an.stage.rig.raster_digest)(store, \*[, art_prefix])             | `digest(src)`: a short content digest for RASTER art, else `None`.                                                                                                                                                                                                                                                |
| [`register_rest_pose_migration`](_autosummary/an.stage.rig.html.md#an.stage.rig.register_rest_pose_migration)(kind, ...)            | Register the protective rest-pose step for one rig kind (an#339).                                                                                                                                                                                                                                                 |
| [`rest_pose_protection`](_autosummary/an.stage.rig.html.md#an.stage.rig.rest_pose_protection)(raw, migrated, \*[, kind])    | What to say when the MIGRATION, on this read, kept a rig's pose unapplied (an#407).                                                                                                                                                                                                                               |
| [`rest_transform`](_autosummary/an.stage.rig.html.md#an.stage.rig.rest_transform)(bone)                               | The node transform fields a bone's rest pose sets (an#339): its `rotation_deg` in radians and its scales, `-0.0` normalised to `0.0`.                                                                                                                                                                             |
| [`rig_affordances`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_affordances)(desc)                              | What a rig's structure affords, derived from the rig model (an#340).                                                                                                                                                                                                                                              |
| [`rig_origin`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_origin)(desc)                                   | The point of the rig, in view_box units, that lands at the entity's placement.                                                                                                                                                                                                                                    |
| [`rig_origin_problems`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_origin_problems)(desc)                          | What is wrong with a rig's declared origin, as warnings (an#338).                                                                                                                                                                                                                                                 |
| [`rig_problems`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_problems)(desc)                                 | What is structurally wrong with a rig's bones and slots (an#340).                                                                                                                                                                                                                                                 |
| [`rig_rest_problems`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_rest_problems)(desc)                            | Warnings about a rig's rest pose (an#339), on a model or a raw document.                                                                                                                                                                                                                                          |
| [`slot_node_paths`](_autosummary/an.stage.rig.html.md#an.stage.rig.slot_node_paths)(desc)                              | `{slot: its node path relative to the entity}` (`torso/arm/hand`), by [`slot_parent_chain()`](_autosummary/an.stage.rig.html.md#an.stage.rig.slot_parent_chain); a slot caught in a cycle maps to its own name.                                                                                                                        |
| [`slot_parent_chain`](_autosummary/an.stage.rig.html.md#an.stage.rig.slot_parent_chain)(desc)                            | `{slot: the slot it nests under, or None}`: THE nesting rule (an#340).                                                                                                                                                                                                                                            |

### Classes

| [`Attachment`](_autosummary/an.stage.rig.html.md#an.stage.rig.Attachment)(\*\*data)   | A drawable: an SVG path + anchor point (in 0..1 per-axis units).                   |
|-------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| [`Bone`](_autosummary/an.stage.rig.html.md#an.stage.rig.Bone)(\*\*data)         | A skeleton joint with a local transform relative to its parent.                    |
| [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument)(\*\*data)  | The base of every document that IS a rig: `PropDescriptor`, `CharacterDescriptor`. |
| [`RigModel`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigModel)(\*\*data)     | Common config: forward-compatible reads, strict writes.                            |
| [`Skin`](_autosummary/an.stage.rig.html.md#an.stage.rig.Skin)(\*\*data)         | A named outfit/variant: maps slot → {attachment_name → Attachment}.                |
| [`Slot`](_autosummary/an.stage.rig.html.md#an.stage.rig.Slot)(\*\*data)         | A draw-order slot bound to a bone, displaying one attachment at a time.            |

### Exceptions

| [`RestPoseWarning`](_autosummary/an.stage.rig.html.md#an.stage.rig.RestPoseWarning)   | A rig's bone rest pose was NOT applied, because the document may predate it.   |
|--------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`RigError`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigError)          | A rig cannot be built as declared (a cycle, or a chain the stage cannot draw). |

### *class* an.stage.rig.Attachment(\*\*data)

Bases: [`RigModel`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigModel)

A drawable: an SVG path + anchor point (in 0..1 per-axis units).

```pycon
>>> a = Attachment(path="parts/head.svg", anchor=(0.5, 0.78))
>>> a.anchor
(0.5, 0.78)
```

#### anchor *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]*

Anchor in 0..1 per-axis units (Pixi’s Sprite.anchor convention).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### source *: [AssetSource](_autosummary/an.ir.assets.html.md#an.ir.assets.AssetSource) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Where THIS part’s art came from, when it is not the descriptor’s
`source` — a character composed from several clips, or a carved head
on a CC0 body, credits each (an#220). `None` = the descriptor’s
`source` covers it. `an credits` lists every one; an all-rights-
reserved part makes the render NOT PUBLISHABLE like any other.
Omitted from the stored document when unset.

#### width *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The size the part draws at, in **view_box units** — the rig’s units,
the ones `x`/`y` and the bones use (an#220). \*\*A declared size
wins\*\* over the art’s own extent, as `Plane.size` does for plates.
Unset, the art’s own extent is the size: an SVG’s `width`/`height`
(else its viewBox), a raster’s PIXEL count — so a PNG carved at one
pixel per unit needs nothing, and one carved at any other scale
declares its size here instead of being resampled. The aspect is the
art’s, always (an#74): with ONE of the two declared the other follows
the art’s aspect; with both, the art is contained in the box
(uniformly scaled to fit, never stretched) and `an character
validate` says when the two aspects disagree. See
[`attachment_box()`](_autosummary/an.stage.rig.html.md#an.stage.rig.attachment_box).

#### x *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Offset from the slot’s bone, in view_box units.

**This is where a part’s position lives**, and it is the reference data
model’s answer, not an invention: DragonBones puts it in
`display.transform`, Spine in the region attachment’s `{x, y}`, and
in both the *slot* carries no transform at all. It is what lets five face
parts share one `head` bone and still land in different places — before
this field they all stacked on the bone, because the descriptor had no
way to say otherwise and the compiler used hardcoded literals instead.

### *class* an.stage.rig.Bone(\*\*data)

Bases: [`RigModel`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigModel)

A skeleton joint with a local transform relative to its parent.

```pycon
>>> b = Bone(name="head", parent="torso", x=0, y=-260, pivot="neck")
>>> b.parent
'torso'
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### pivot *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Optional pivot name — must match a circle in the SVG `skeleton` group.

### an.stage.rig.CHARACTER_ART_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'characters/'*

The `assets.textures` `src` prefix a rig’s art is addressed under, which is
also the mall store that resolves it (`render.ASSET_SRC_PREFIX_TO_STORE`).
A parameter rather than a literal because the rig builder is the same code
for a character and for a prop, and the store is the ONLY thing that differs
about where their art lives. Two hardcoded copies of `"characters/"` — the
`src` builder and the probe’s own — reached three call sites, and that is
what made “a prop is a rig too” read as a rewrite instead of an argument
(an#108).

### an.stage.rig.CONTAIN_FIT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'contain'*

The fit policy every compiled sprite carries. Named rather than inlined so
the one place that decides “the art keeps its shape” is greppable.

### an.stage.rig.DEFAULT_VIEW_BOX *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int)]* *= (0, 0, 1024, 1024)*

1024x1024 with feet near y≈980. All parts
inherit this viewBox at export so PixiJS can use the SVG’s intrinsic
viewBox without a calibration step.

* **Type:**
  Canonical character viewBox

### an.stage.rig.NESTINGS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('flat', 'bones')*

The nesting modes ([`RigDocument.nesting`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument.nesting)); unset means the first.

### an.stage.rig.PROP_ART_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'props/'*

The same, for props. Both are keys of `render.ASSET_SRC_PREFIX_TO_STORE`,
which is what decides where the staging step copies the art from.

### an.stage.rig.REST_POSE_SINCE *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'PropDescriptor': '0.2.0'}*

`{rig document kind: the version from which its bones' rest pose is applied}`,
filled by [`register_rest_pose_migration()`](_autosummary/an.stage.rig.html.md#an.stage.rig.register_rest_pose_migration) (an#339). The builder guard
reads it to recognise a document older than that version.

### an.stage.rig.RIG_DOCUMENT_OPTIONAL_FIELDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('origin', 'rest_rotation', 'nesting')*

The fields [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument) adds, each written out of the stored
document when unset ([`omit_unset_rig_fields()`](_autosummary/an.stage.rig.html.md#an.stage.rig.omit_unset_rig_fields)), so every descriptor
that never set one reads back, and hashes, as it did before the field existed.

### an.stage.rig.RIG_HIERARCHY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'rig.hierarchy'*

The capability a nested chain affords (ADR 0002 decision 1’s name, registered
by the core in [`an.capabilities.subjects`](_autosummary/an.capabilities.subjects.html.md#module-an.capabilities.subjects)).

### *exception* an.stage.rig.RestPoseWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A rig’s bone rest pose was NOT applied, because the document may predate it.

### *class* an.stage.rig.RigDocument(\*\*data)

Bases: [`RigModel`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigModel)

The base of every document that IS a rig: `PropDescriptor`, `CharacterDescriptor`.

Holds what is true of the rig as a whole, not of one bone or slot (which is
why it is not [`RigModel`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigModel), the base of [`Bone`](_autosummary/an.stage.rig.html.md#an.stage.rig.Bone) and the others
as well). Every field here is omitted from the stored document when unset.

```pycon
>>> RigDocument(origin=(512, 1010)).origin
(512.0, 1010.0)
>>> RigDocument().model_dump()
{}
>>> RigDocument(origin=(float("inf"), 0))
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for RigDocument
...
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### nesting *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['flat', 'bones'] | [None](https://docs.python.org/3/builtins/constants.html#None)*

a slot nests under its
OWN bone’s primary slot only, so limbs are siblings of the torso (the
rigs’ long-standing shape). `"bones"`: a slot nests under the primary
slot of the nearest ancestor bone that has one, to any depth, so a
forearm turns with its upper arm and a sword with its hand (forward
kinematics). [`slot_parent_chain()`](_autosummary/an.stage.rig.html.md#an.stage.rig.slot_parent_chain) is the rule.

* **Type:**
  How slots nest (an#340). Unset or `"flat"`

#### origin *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[float](https://docs.python.org/3/builtins/functions.html#float), FieldInfo(annotation=NoneType, required=True, metadata=[AllowInfNan(allow_inf_nan=False)])], [Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)[[float](https://docs.python.org/3/builtins/functions.html#float), FieldInfo(annotation=NoneType, required=True, metadata=[AllowInfNan(allow_inf_nan=False)])]] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The point of the art that the entity’s placement (`stage.at`) refers
to, in view_box units (an#338). Unset, the rig is placed by the centre
of its bones’ extent ([`bone_extent_centre()`](_autosummary/an.stage.rig.html.md#an.stage.rig.bone_extent_centre)), which is what every
rig did before the field existed; a prop declares its foot (a tripod’s,
a figurine’s stand) so it stands where it is put whatever its extent.

#### rest_rotation *: [bool](https://docs.python.org/3/builtins/functions.html#bool) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Whether the bones’ `rotation_deg`/`scale_x`/`scale_y` pose the
built parts (an#339). Unset (or `True`) they do: the rest pose is the
bones’. `False` is written ONLY by the migration onto a document from
before that rule whose bones carry a rotation or scale, because such a
rig was drawn with the pose already in its pixels (the fields were
ignored) and applying them now would pose it twice
([`protect_legacy_rest_pose()`](_autosummary/an.stage.rig.html.md#an.stage.rig.protect_legacy_rest_pose)).

### *exception* an.stage.rig.RigError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A rig cannot be built as declared (a cycle, or a chain the stage cannot draw).

### *class* an.stage.rig.RigModel(\*\*data)

Bases: `BaseModel`

Common config: forward-compatible reads, strict writes.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.stage.rig.SCENE_PX_PER_VIEW_BOX *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 345.0*

Scene-graph pixels spanned by a descriptor’s full `view_box` height.

The single number that maps descriptor space to scene space. One uniform
factor `k = SCENE_PX_PER_VIEW_BOX / view_box_height` scales bone positions
and part extents alike — uniform by construction, so the compiler cannot
violate the invariant that aspect ratio is intrinsic to the art (an#74).

345 is a calibration, not a preference. It is what reproduces the framing the
seven deleted `_SVG_*_SIZE` constants hand-tuned: at k = 345/1024 = 0.3369,
`saturated-rig`’s own art gives torso 107.8x129.4 against the old 110x130,
legs 37.7x118.6 against 38x120. The constants were an approximation of
exactly this product, which is the evidence that the rig should have been
driving it all along.

### *class* an.stage.rig.Skin(\*\*data)

Bases: [`RigModel`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigModel)

A named outfit/variant: maps slot → {attachment_name → Attachment}.

```pycon
>>> skin = Skin(name="default", slots={"mouth": {"mouth_a": Attachment(path="parts/mouth/mouth_a.svg")}})
>>> skin.slots["mouth"]["mouth_a"].path
'parts/mouth/mouth_a.svg'
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.rig.Slot(\*\*data)

Bases: [`RigModel`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigModel)

A draw-order slot bound to a bone, displaying one attachment at a time.

```pycon
>>> s = Slot(name="mouth", bone="head", draw_order=7, attachment="mouth_x")
>>> s.attachment
'mouth_x'
```

#### attachment *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Default attachment name; the active attachment can change at runtime
via animation tracks targeting `slot:<name>.attachment`.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.stage.rig.art_src(ref, rel_path, , art_prefix='characters/')

Path used inside the runtime dir, relative to `index.html`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> art_src("maya", "parts/head.svg")
'characters/maya/parts/head.svg'
>>> art_src("lamp", "parts/body.svg", art_prefix="props/")
'props/lamp/parts/body.svg'
```

### an.stage.rig.attachment_box(width, height, art)

The box a part draws in, in view_box units: the declared size wins, the
art’s aspect is kept (an#220).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> attachment_box(None, None, (40, 20))       # the art's own extent
(40.0, 20.0)
>>> attachment_box(120, None, (40, 20))        # width declared: height follows
(120.0, 60.0)
>>> attachment_box(None, 30, (40, 20))
(60.0, 30.0)
>>> attachment_box(120, 120, (40, 20))         # both: contained, never stretched
(120.0, 60.0)
>>> attachment_box(120, 90, None)              # unmeasurable art: the box as declared
(120.0, 90.0)
>>> attachment_box(120, None, None) is None    # nothing to take the aspect from
True
```

### an.stage.rig.bone_extent_centre(bones)

The DEFAULT point in view_box space that the entity’s placement refers
to, when the rig declares no [`RigDocument.origin`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument.origin) ([`rig_origin()`](_autosummary/an.stage.rig.html.md#an.stage.rig.rig_origin)).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> bone_extent_centre({"root": (512.0, 980.0), "head": (512.0, 420.0)})
(512.0, 700.0)
```

The centre of the rig’s bone extent, **not** the root bone. The scene root
positions a character on x only and leaves y at 0, so this point is what
lands at the frame’s vertical centre — and a rig whose root is its ground
contact (the default puts it at the feet, y=980) would therefore hang its
whole body above the placement point, head off-frame.

Centring on the extent makes framing independent of where an author chose
to put the root, which is a rigging decision and should not be a framing
one. On the default rig it lands at y=700, within 20 units of the torso
bone — i.e. it reproduces the convention the deleted `torso_y = 0.0`
literal encoded, without hardcoding a bone name.

### an.stage.rig.bone_positions(desc)

Absolute `(x, y)` per bone, in view_box units.

Bone transforms are parent-relative, so a bone’s position is the sum along
its parent chain. A cycle or a dangling parent stops the walk rather than
looping — a malformed rig is #78’s business, not this function’s.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### an.stage.rig.bones_carry_a_rest_pose(doc)

Whether any bone of a (raw or model) rig document has a non-zero
`rotation_deg` or a non-unit `scale_x`/`scale_y`.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> bones_carry_a_rest_pose({"bones": [{"name": "a"}, {"name": "b", "rotation_deg": 22}]})
True
>>> bones_carry_a_rest_pose({"bones": [{"name": "a", "scale_x": 1.0}]})
False
```

### an.stage.rig.build_rig_subtree(entity, desc_data, , textures, probe=None, resolutions=None, art_prefix='characters/', descriptor_model, document_kind, texture_srcs=None, digest=None, skip_slots=None)

Build the scene subtree for a rig (a prop or a character), **from its descriptor**.

Slots nest by [`slot_parent_chain()`](_autosummary/an.stage.rig.html.md#an.stage.rig.slot_parent_chain) (`nesting: flat` or `bones`,
an#340). In `bones` mode a nested part is placed relative to its parent
BONE, without inheriting the parent’s attachment offset, and its bone’s
rest pose composes through the chain.

`skip_slots` are slots the GENRE says not to build (the cut-out genre’s
baked face: `cutan.characters.play.suppressed_slots`); a skipped slot’s
nested parts are not built either. `None` is the legacy rule for a genre
that predates the argument (`an.genres.API_LEVEL` < 5): with
`face_overlay` false, the slots nested under the `head` bone’s primary
slot. It is the one place the core still names a bone, and it goes when no
genre needs it.

A part may be SVG or raster (PNG/JPEG/WebP, an#211): the probe measures
either, and `digest(src)` — a content digest for raster art, `None`
for SVG — is appended to a raster texture’s alias so the texture is
addressed by its bytes.

**Every swap key keeps its own geometry** (an#211). A swap re-textures the
sprite, and the box, anchor and offset were the DEFAULT attachment’s, so a
key drawn on a different canvas was fitted into the wrong box — a closed
mouth on a thin canvas squashed every open mouth to a fraction of a pixel.
A key whose box, anchor or offset differs from the drawn attachment’s is
listed in `VisualJSON.asset_geometry` and the runtime applies it with the
texture; a rig whose keys share a canvas emits nothing new.

`texture_srcs` maps a part path to the `src` its texture loads from
instead of the stored file — a style pack’s recoloured art
(`_recoloured_texture_srcs()`). Such a texture’s alias carries a digest
of its content, so a different recolour is a different texture (the
runtime’s loader ignores a re-added alias on hot reload, an#155). The part
is still probed and sized from the stored file, whose geometry is the same.

Every part’s position comes from a bone, every part’s extent from its own
art, and both are scaled by one uniform factor. Nothing here is a module
constant: the seven `_SVG_*_SIZE` values and the four y-offset literals
this replaced are gone, and gutting `bones`/`slots`/`skins`/`view_box`
now changes the output — which it provably did not before (an#73).

A slot whose art is not on disk is recorded in `resolutions` as a fallback,
which makes it audible by default and fatal under `strict_assets` — the
same treatment a missing *character* already got (an#33), now reaching
inside the descriptor to the individual part (an#76). It is recorded rather
than raised here because the decision belongs to one place, and that place
is `_raise_or_warn_on_asset_fallbacks()`.

`probe(src) -> (exists, size)` answers whether a part’s art is on disk and
what size it rasterises at. Existence decides whether a texture is declared
at all. The sprite’s box is the attachment’s declared `width`/`height`
when it has them — **a declared size wins**, with the art’s aspect kept
(`an.characters.schema.attachment_box()`, an#220) — else the art’s own
extent (a raster’s pixel count); failing both, the runtime’s `contain`
fit draws the art at its natural shape — never stretched to a fabricated box.

* **Return type:**
  [`NodeJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.NodeJSON)

### an.stage.rig.chain_draw_order_problems(desc)

Where a nested chain asks the STAGE for a draw order it cannot give (an#340, an#403).

The stage sorts the items of each container ([`chain_paint_order()`](_autosummary/an.stage.rig.html.md#an.stage.rig.chain_paint_order)), so
a part nested under one drawn LATER is fine (a far arm behind its torso).
What it cannot do is interleave two containers: a slot’s subtree is always
painted together, so an unrelated part ordered between a chain’s members
(a leg between the arms and the head) cannot be honoured. Refused by the
stage compiler; the asset validators only warn. Nothing in `flat`
nesting.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from types import SimpleNamespace as NS
>>> rig = NS(nesting="bones",
...          bones=[NS(name="arm", parent=None), NS(name="hand", parent="arm"), NS(name="cord", parent=None)],
...          slots=[NS(name="arm", bone="arm", draw_order=1), NS(name="cord", bone="cord", draw_order=2),
...                 NS(name="hand", bone="hand", draw_order=3)])
>>> chain_draw_order_problems(rig)[0].startswith("the stage paints each chain's parts together")
True
```

### an.stage.rig.chain_paint_order(desc, , built=None)

How each container of a nested chain must order what it holds (an#403).

The stage draws a container’s items in order: its slot’s own visual and its
child slots’ containers (`None` is the entity’s container, which holds the
root slots and no visual of its own). Sorting those siblings (PixiJS’s
`sortableChildren` on a `zIndex`) gives any order in which every slot’s
SUBTREE is painted contiguously, a far arm behind the torso it nests under
included. Each item spans the draw orders of its subtree; the items are
sorted, stably, by that `(lowest, highest)` span, starting from the
builder’s own order ([`natural_paint_order()`](_autosummary/an.stage.rig.html.md#an.stage.rig.natural_paint_order)), so equal draw orders keep
their tree order and a chain already painted in declared order is not
reordered. `built` limits it to the slots the builder drew.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

```pycon
>>> from types import SimpleNamespace as NS
>>> rig = NS(nesting="bones",
...          bones=[NS(name="torso", parent=None), NS(name="arm", parent="torso")],
...          slots=[NS(name="torso", bone="torso", draw_order=2), NS(name="arm", bone="arm", draw_order=1)])
>>> chain_paint_order(rig)
{None: ['torso'], 'torso': ['arm', 'torso']}
```

### an.stage.rig.chain_pose_problems(desc)

A rest pose the stage cannot apply in a nested chain (an#340).

A node exists only for a bone that carries a slot. A bone with NO slot
between a part and the part it nests under has no node to rotate or
scale, so its `rotation_deg`/`scale_*` would be lost (its `x`/`y`
are kept: positions sum along the chain). Refused at compile, like the
draw order; give the bone a slot or move its pose to the bone below it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from types import SimpleNamespace as NS
>>> rig = NS(nesting="bones", slots=[NS(name="arm", bone="arm"), NS(name="hand", bone="hand")],
...          bones=[NS(name="arm", parent=None), NS(name="elbow", parent="arm", rotation_deg=-70),
...                 NS(name="hand", parent="elbow")])
>>> chain_pose_problems(rig)[0].startswith("bone 'elbow' carries no slot")
True
```

### an.stage.rig.declared_origin(desc)

The rig’s DECLARED origin as two floats, or `None` when it declares none.

`desc` is a model or a raw document (a mapping, as `an validate` reads
it); a descriptor model that predates [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument), where
`origin` is an `extra` key (a list), is read the same way.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> from types import SimpleNamespace as NS
>>> declared_origin(NS(origin=[512, 1010])), declared_origin(NS())
((512.0, 1010.0), None)
>>> declared_origin({"origin": [0, 5]})
(0.0, 5.0)
```

### an.stage.rig.drawn_attachment(desc, skin, slot)

The `(name, attachment)` a slot draws by default, or `None`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Attachment`](_autosummary/an.stage.rig.html.md#an.stage.rig.Attachment)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.stage.rig.global_paint_order(desc, , built=None)

The node paths of a rig’s slots, relative to its entity, in DECLARED
draw order: the paint list the stage uses when a nested chain interleaves
with an unrelated part (an#430), which no sort of a container’s own items
can give ([`chain_draw_order_problems()`](_autosummary/an.stage.rig.html.md#an.stage.rig.chain_draw_order_problems)). Spine’s global slot order:
each part follows its bone’s world transform, and the list says who
paints over whom. `built` limits it to the slots the builder drew.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from types import SimpleNamespace as NS
>>> rig = NS(nesting="bones",
...          bones=[NS(name="arm", parent=None), NS(name="hand", parent="arm"), NS(name="cord", parent=None)],
...          slots=[NS(name="arm", bone="arm", draw_order=1), NS(name="cord", bone="cord", draw_order=2),
...                 NS(name="hand", bone="hand", draw_order=3)])
>>> global_paint_order(rig)
['arm', 'cord', 'arm/hand']
```

### an.stage.rig.legacy_rest_pose_unknown(raw, kind, , since)

The builder guard (an#339): whether `raw` may be a pre-rest-pose
document that its migration could not see.

`DocumentKind.version_of` reads a document with no version field as
CURRENT, so the protective migration never runs on it. A document like
that whose bones carry a pose, and which says nothing about
`rest_rotation`, is built flat (as it would have been) and warned
about. A version older than `since` is caught too, should a read skip
the migration.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> k = DocumentKind("Demo", "schema_version", "0.2.0")
>>> legacy_rest_pose_unknown({"bones": [{"name": "a", "rotation_deg": 9}]}, k, since="0.2.0")
True
>>> legacy_rest_pose_unknown({"schema_version": "0.2.0", "bones": [{"name": "a", "rotation_deg": 9}]}, k, since="0.2.0")
False
```

### an.stage.rig.natural_paint_order(order, keys)

What each container holds in the builder’s own order: the slot’s visual
first, then its children by their own `(draw_order, name)`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.stage.rig.nesting_of(desc)

`"flat"` or `"bones"` (an#340); unset is flat.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> nesting_of({"nesting": "bones"}), nesting_of({})
('bones', 'flat')
```

### an.stage.rig.omit_unset_rig_fields(data)

Drop every unset [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument) field from a dumped document, in place.

A subclass that declares its own `model_serializer` REPLACES the base’s
(pydantic keeps one per model), so it must call this on its output, or an
unset `origin` reaches every stored descriptor as `null`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

```pycon
>>> omit_unset_rig_fields({"name": "lamp", "origin": None})
{'name': 'lamp'}
>>> omit_unset_rig_fields({"origin": [512.0, 1010.0]})
{'origin': [512.0, 1010.0]}
```

### an.stage.rig.part_probe(characters_store, , art_prefix='characters/')

A probe answering `(art exists, the size it rasterises at)` for a part.

**Two questions, deliberately not one.** Whether the art is *there* decides
whether the compiler declares a texture for it; whether it can be *measured*
decides only whether the sprite’s box comes from the art or from the
runtime’s fit. Collapsing them is a real bug and it was here: a degenerate
`<svg/>` is unmeasurable but present, and treating that as absent made the
part vanish from the scene silently — trading an#79’s hang for exactly the
invisible-art failure #76 exists to stop.

Returns `None` when the store has no filesystem root — **not** a probe
that answers “absent” — because a store that can answer nothing must drop
no parts rather than all of them.

Size is read from the SVG root’s `width`/`height`, falling back to the
viewBox extent as a browser does — or, for PNG/JPEG/WebP art, from the
image header (an#211): a header parse, not a render, either way. Before
an#211 a PNG was parsed AS SVG here and the compile died on an XML error.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`bool`](https://docs.python.org/3/builtins/functions.html#bool), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)]]]

### an.stage.rig.primary_slot_per_bone(desc)

`{bone name: the slot that IS that bone}`, when one exists.

The anchor of node nesting ([`slot_parent_chain()`](_autosummary/an.stage.rig.html.md#an.stage.rig.slot_parent_chain)). In the default
`flat` nesting the rigs are flat — arms are siblings of the torso, not
children (CLAUDE.md pillar 4) — so bone parentage decides *position* only:
a slot nests under the primary slot of its own bone when it is not that
slot itself, which puts eyes and mouth under `head` and leaves every limb
a direct child of the entity. `nesting: bones` (an#340) follows the bone
hierarchy to the nearest ancestor’s primary slot instead.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from types import SimpleNamespace as NS
>>> primary_slot_per_bone(NS(slots=[NS(name="head", bone="head"), NS(name="mouth", bone="head")]))["head"]
'head'
```

### an.stage.rig.protect_legacy_rest_pose(doc)

The migration step every rig kind runs onto the version that applies the rest pose (an#339).

Before it, a bone’s `rotation_deg` and scale reached no node, so a rig
whose bones carry one was drawn with that pose in its pixels. Such a
document gets `rest_rotation: false` and keeps its picture; a document
whose bones carry none gets nothing (and every document on the
maintainer’s machine was of that kind when this shipped). Returns `doc`,
edited in place; the caller sets the version.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> protect_legacy_rest_pose({"bones": [{"name": "leg", "rotation_deg": -22}]})["rest_rotation"]
False
>>> "rest_rotation" in protect_legacy_rest_pose({"bones": [{"name": "root"}]})
False
>>> protect_legacy_rest_pose({"bones": [{"name": "leg", "rotation_deg": 5}], "rest_rotation": True})["rest_rotation"]
True
```

### an.stage.rig.raster_digest(store, , art_prefix='characters/')

`digest(src)`: a short content digest for RASTER art, else `None`.

A raster texture is addressed by its bytes (an#211): the digest goes into
the texture’s alias, so a re-carved part is a different texture — the
runtime’s loader ignores a re-added alias on hot reload (an#155) — and a
different compiled contract, whose hash then covers the pixels drawn. SVG
art keeps its plain alias, which is what keeps every existing document
byte-identical.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

### an.stage.rig.register_rest_pose_migration(kind, from_version, to_version)

Register the protective rest-pose step for one rig kind (an#339).

One call from the module that owns the kind’s schema (`an.stage.props`
for `PropDescriptor`, `cutan` for `CharacterDescriptor`): it registers
[`protect_legacy_rest_pose()`](_autosummary/an.stage.rig.html.md#an.stage.rig.protect_legacy_rest_pose) as that kind’s `from -> to` migration and
records `to` as the version the builder applies the rest pose from.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> register_rest_pose_migration("DemoRig", "1.0", "2.0")
>>> REST_POSE_SINCE["DemoRig"]
'2.0'
>>> from an.ir.migrate import MIGRATIONS
>>> MIGRATIONS[("DemoRig", "1.0", "2.0")]({"version": "1.0", "bones": []})
{'version': '1.0', 'bones': []}
```

### an.stage.rig.rest_pose_protection(raw, migrated, , kind='rig document')

What to say when the MIGRATION, on this read, kept a rig’s pose unapplied (an#407).

The protective step writes `rest_rotation: false` onto a document from
before the rest pose whose bones carry one, guessing the pose is already in
its pixels. An author who added a rotation to such a file is the other
case, and nothing else would tell them why the part did not move. A flag
the AUTHOR wrote (present in the stored document) is a choice, and silent.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> old = {"schema_version": "0.1.0", "bones": [{"name": "leg", "rotation_deg": 20}]}
>>> rest_pose_protection(old, {**old, "rest_rotation": False}).startswith("its bones carry")
True
>>> rest_pose_protection({**old, "rest_rotation": False}, {**old, "rest_rotation": False}) is None
True
```

### an.stage.rig.rest_transform(bone)

The node transform fields a bone’s rest pose sets (an#339): its
`rotation_deg` in radians and its scales, `-0.0` normalised to `0.0`.

Every rest reader composes on the built transform (`play` deviations,
swap poses, the face solver, presets, a from-less tween, the runtime’s
load), so this is the whole change. An authored `rotation` tween stays
ABSOLUTE: `to: 0` straightens a part whose rest is splayed.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> rest_transform(Bone(name="leg", rotation_deg=-22))["rotation"]
-0.3839724354387525
>>> rest_transform(Bone(name="leg", rotation_deg=-0.0))
{'rotation': 0.0, 'scale_x': 1.0, 'scale_y': 1.0}
```

### an.stage.rig.rig_affordances(desc)

What a rig’s structure affords, derived from the rig model (an#340).

`rig.hierarchy` when the rig nests in `bones` mode and some chain links
two different bones: `keys` are every slot in such a chain (so
`rig.hierarchy:forearm_l` asks for a forearm in a chain), `count` the
deepest chain’s number of BONES (`rig.hierarchy>=3` is “a shoulder, an
elbow and a hand”), `chains` every root-to-leaf chain of slots. Parts on
one bone (a head with its face) add no depth, and a flat rig affords
nothing here: that nesting is a drawing convention, not a joint. The core registers it as the `prop`
analyser; a genre’s analyser composes it for its own kinds.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

```pycon
>>> from types import SimpleNamespace as NS
>>> rig = NS(nesting="bones", bones=[NS(name="arm", parent=None), NS(name="hand", parent="arm")],
...          slots=[NS(name="arm", bone="arm"), NS(name="hand", bone="hand"), NS(name="sword", bone="hand")])
>>> rig_affordances(rig)
{'rig.hierarchy': {'keys': ['arm', 'hand', 'sword'], 'count': 2, 'chains': [['arm', 'hand', 'sword']]}}
```

### an.stage.rig.rig_origin(desc)

The point of the rig, in view_box units, that lands at the entity’s placement.

The ONE rule (an#338): the declared [`RigDocument.origin`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument.origin) when the rig
has one, else the centre of its bones’ extent ([`bone_extent_centre()`](_autosummary/an.stage.rig.html.md#an.stage.rig.bone_extent_centre)).
[`build_rig_subtree()`](_autosummary/an.stage.rig.html.md#an.stage.rig.build_rig_subtree) places every part relative to it, and a genre that
reports where a rig’s art reaches from its stage point (`cutan`’s
`stage_extent`) reads it too, so the two agree by construction.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> from types import SimpleNamespace as NS
>>> bones = [NS(name="root", parent=None, x=512, y=980), NS(name="top", parent="root", x=0, y=-560)]
>>> rig_origin(NS(bones=bones))
(512.0, 700.0)
>>> rig_origin(NS(bones=bones, origin=(512, 980)))
(512.0, 980.0)
```

### an.stage.rig.rig_origin_problems(desc)

What is wrong with a rig’s declared origin, as warnings (an#338).

Nothing when the rig declares none. A non-finite origin (possible only on a
document read without [`RigDocument`](_autosummary/an.stage.rig.html.md#an.stage.rig.RigDocument), which refuses one) places the
whole rig nowhere; one outside the `view_box` is legal (a hanging sign’s
hook can sit above its art) but is far more often a unit slip (scene pixels
written where view_box units belong), so it is said, not refused.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from types import SimpleNamespace as NS
>>> rig_origin_problems(NS(origin=(512, 1010), view_box=(0, 0, 1024, 1024)))
[]
>>> rig_origin_problems(NS(origin=(512, 2000), view_box=(0, 0, 1024, 1024)))[0].startswith("origin (512.0, 2000.0) lies outside")
True
>>> rig_origin_problems(NS(origin=(float("nan"), 0), view_box=(0, 0, 1024, 1024)))[0][:30]
'origin (nan, 0.0) is not finit'
```

### an.stage.rig.rig_problems(desc)

What is structurally wrong with a rig’s bones and slots (an#340).

A bone whose `parent` names no bone; a cycle in the bone graph (the one
form a closed linkage can take in a model where each bone names one
parent: forward kinematics only, so it is refused); a slot whose `bone`
names no bone (it used to land at the origin, silently). Shared by the
asset validators (`an character validate`, `an.stage.props.validate_prop()`).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from types import SimpleNamespace as NS
>>> rig_problems(NS(bones=[NS(name="a", parent="b"), NS(name="b", parent="a")],
...                 slots=[NS(name="s", bone="nope")]))
['bones form a cycle (a closed linkage): a -> b -> a; a rig is a tree (forward kinematics only)', "slot 's' is bound to bone 'nope', which the rig does not declare"]
```

### an.stage.rig.rig_rest_problems(desc)

Warnings about a rig’s rest pose (an#339), on a model or a raw document.

A part turns about its NODE’s origin, which is the bone plus the drawn
attachment’s `x`/`y` offset. A bone with a rest rotation whose part is
offset therefore turns that part about a point that is not the joint, the
usual way a splayed leg ends up detached from its hip. The way to turn a
part about its joint is `x: 0, y: 0` on the attachment and the art’s
`anchor` at the joint. Nothing is said about a rig that keeps its legacy
pose (`rest_rotation: false`), since its bones pose nothing.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> doc = {"bones": [{"name": "leg", "rotation_deg": 20}],
...        "slots": [{"name": "leg", "bone": "leg"}],
...        "skins": {"default": {"slots": {"leg": {"leg": {"path": "p.svg", "y": 40}}}}}}
>>> rig_rest_problems(doc)[0].startswith("bone 'leg' rests at 20")
True
>>> doc["skins"]["default"]["slots"]["leg"]["leg"]["y"] = 0
>>> rig_rest_problems(doc)
[]
```

### an.stage.rig.slot_node_paths(desc)

`{slot: its node path relative to the entity}` (`torso/arm/hand`), by
[`slot_parent_chain()`](_autosummary/an.stage.rig.html.md#an.stage.rig.slot_parent_chain); a slot caught in a cycle maps to its own name.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from types import SimpleNamespace as NS
>>> rig = NS(nesting="bones", bones=[NS(name="a", parent=None), NS(name="b", parent="a")],
...          slots=[NS(name="a", bone="a"), NS(name="b", bone="b")])
>>> slot_node_paths(rig)
{'a': 'a', 'b': 'a/b'}
```

### an.stage.rig.slot_parent_chain(desc)

`{slot: the slot it nests under, or None}`: THE nesting rule (an#340).

The builder and a genre’s part paths (`cutan`’s `play.slot_parent`,
`slot_node_path`) all read it, so a `play` or a preset addresses the
node the builder made.

- `flat` (unset): a slot nests under its own bone’s primary slot (the
  slot named like the bone) when it is not that slot; everything else is a
  child of the entity. Arms are siblings of the torso.
- `bones`: the same, and a slot that IS its bone’s primary (or whose bone
  has none) nests under the primary slot of the nearest ANCESTOR bone that
  has one, to any depth. A bone cycle stops the walk; `rig_problems`
  names it and the builder refuses it.

```pycon
>>> from types import SimpleNamespace as NS
>>> rig = NS(bones=[NS(name="arm", parent="torso"), NS(name="hand", parent="arm"),
...                 NS(name="torso", parent=None)],
...          slots=[NS(name="torso", bone="torso"), NS(name="arm", bone="arm"),
...                 NS(name="hand", bone="hand"), NS(name="sword", bone="hand")])
>>> slot_parent_chain(rig)
{'torso': None, 'arm': None, 'hand': None, 'sword': 'hand'}
>>> rig.nesting = "bones"
>>> slot_parent_chain(rig)
{'torso': None, 'arm': 'torso', 'hand': 'arm', 'sword': 'hand'}
```

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]


# _autosummary/an.stage.runtime.html.md

# an.stage.runtime

Cutout JS runtime — see README.md in this directory.


# _autosummary/an.stage.runtime_files.html.md

# an.stage.runtime_files

Locate the bundled cutout JS runtime files.

The runtime ships under `an/stage/runtime/` and is consumed by the
headless renderer in Phase 2C. This module exposes paths so callers don’t
hard-code the layout.

```pycon
>>> p = runtime_dir()
>>> p.is_dir()
True
>>> (p / "index.html").is_file()
True
```

### Functions

| [`runtime_dir`](_autosummary/an.stage.runtime_files.html.md#an.stage.runtime_files.runtime_dir)()        | Return the directory containing index.html + runtime.js.   |
|-----------------------------------------------------------------------|------------------------------------------------------------|
| [`runtime_index_html`](_autosummary/an.stage.runtime_files.html.md#an.stage.runtime_files.runtime_index_html)() | Path to `index.html`.                                      |
| [`runtime_js`](_autosummary/an.stage.runtime_files.html.md#an.stage.runtime_files.runtime_js)()         | Path to `runtime.js`.                                      |

### an.stage.runtime_files.runtime_dir()

Return the directory containing index.html + runtime.js.

Uses importlib.resources so it works from a wheel install too.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.stage.runtime_files.runtime_index_html()

Path to `index.html`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### an.stage.runtime_files.runtime_js()

Path to `runtime.js`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/an.stage.serialize.html.md

# an.stage.serialize

JSON contract between the Python compiler and the (future) JS runtime.

These Pydantic models describe **exactly** the JSON shape the PixiJS runtime
consumes — nothing more. `compile_shot` produces these objects; the runtime
reads them. The schema is deliberately separate from the cutout-internal
Python evaluation types (`clip.Clip`, `channel.Channel`) so the Python side
can evolve internally without breaking the runtime contract.

This file used to carry a sketched Spine-flavoured swap vocabulary
(`SkinJSON`, `RigJSON`, `CutoutSceneJSON.rigs`, `SlotJSON`/
`NodeJSON.slots`, `current_attachment`) that nothing populated and nothing
read — deleted in an#86 so the *real* swap mechanism (`VisualJSON`’s
per-node asset maps driven by step channels) is the only one the contract
describes. A few declared-but-unwired scalars remain (`PlacedClipJSON.blend_in`
/ `blend_out` — recorded, never applied; `VisualJSON.texture_id`): the same
debt class at smaller scale, kept only because they are field-shaped
placeholders rather than a parallel *vocabulary* for a capability that shipped
elsewhere. Do not add more; a new field needs its producer and its consumer in
the same change.

```pycon
>>> j = CutoutSceneJSON(
...     meta={"fps": 30, "width": 1920, "height": 1080, "duration": 5.0},
...     scene=NodeJSON(name="root"),
...     animations={},
...     timeline=TimelineJSON(duration=5.0, tracks=[]),
...     assets=AssetsJSON(textures={}, audio={}),
... )
>>> from_dict(to_dict(j)) == j
True
```

### Module Attributes

| [`BUILTIN_VISUAL_KINDS`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.BUILTIN_VISUAL_KINDS)   | The visual kinds `runtime.js` draws itself.   |
|-------------------------------------------------------------------------|-----------------------------------------------|

### Functions

| [`from_dict`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.from_dict)(d)   | Rebuild a scene from a plain-dict representation.              |
|-----------------------------------------------------------------|----------------------------------------------------------------|
| [`to_dict`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.to_dict)(scene) | Dump a scene to a plain-dict representation (no None pruning). |

### Classes

| [`AnimationClipJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.AnimationClipJSON)(\*\*data)   | A named, reusable animation clip.                                                                                          |
|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| [`AssetJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.AssetJSON)(\*\*data)           | A single asset (texture / audio file).                                                                                     |
| [`AssetResolutionJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.AssetResolutionJSON)(\*\*data) | How one scene entity's store reference actually resolved at compile time.                                                  |
| [`AssetsJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.AssetsJSON)(\*\*data)          | Map of asset id → AssetJSON, split by kind.                                                                                |
| [`ChannelJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.ChannelJSON)(\*\*data)         | One animated property of one target.                                                                                       |
| [`CutoutSceneJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.CutoutSceneJSON)(\*\*data)     | Top-level cutout scene JSON — the JS runtime's input contract.                                                             |
| [`CutoutSceneMetaJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.CutoutSceneMetaJSON)(\*\*data) | Per-shot metadata.                                                                                                         |
| [`KeyframeJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.KeyframeJSON)(\*\*data)        | Single keyframe in an animation channel.                                                                                   |
| [`NodeJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.NodeJSON)(\*\*data)            | One node in the scene tree.                                                                                                |
| [`PathJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.PathJSON)(\*\*data)            | A stroked path's drawing instruction (an#160), carried on a `path` visual.                                                 |
| [`PlacedClipJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.PlacedClipJSON)(\*\*data)      | An animation placed on a track at a specific time.                                                                         |
| [`TimelineJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.TimelineJSON)(\*\*data)        | Top-level timeline: total duration + tracks.                                                                               |
| [`TrackJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.TrackJSON)(\*\*data)           | A sequence of placed clips with optional target-prefix metadata.                                                           |
| [`TransformJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.TransformJSON)(\*\*data)       | Local transform of a scene-graph node (authoring form).                                                                    |
| [`UnderlayJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.UnderlayJSON)(\*\*data)        | A copy of a node's own visual, drawn BEHIND it in the same container (an#163 gap 5: the outline and the paper-gap shadow). |
| [`VisualJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.VisualJSON)(\*\*data)          | Drawable content attached to a node.                                                                                       |

### *class* an.stage.serialize.AnimationClipJSON(\*\*data)

Bases: `_JSONModel`

A named, reusable animation clip.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.AssetJSON(\*\*data)

Bases: `_JSONModel`

A single asset (texture / audio file).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.AssetResolutionJSON(\*\*data)

Bases: `_JSONModel`

How one scene entity’s store reference actually resolved at compile time.

The IR declares every drawable entity as `store` + `ref`. What the
compiler *builds* from that pair is not recoverable from the scene tree
afterwards: a character whose descriptor is missing and a character that
never had one produce byte-identical procedural rigs. That ambiguity is
an#33 — three CI runners once agreed perfectly about a picture that was not
the picture, and the agreement read as a clean positive result.

So the compiler records what it did, per entity, in the artifact the
browser actually loads. `fallback` is the load-bearing bit: True means
the declared ref supplied nothing and a stand-in was drawn in its place.

```pycon
>>> AssetResolutionJSON(
...     id="maya", kind="character", store="characters", ref="maya-v1",
...     resolved="descriptor", fallback=False,
... ).fallback
False
```

#### detail *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

One human sentence saying why, when `fallback` is True.

#### fallback *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when the declared ref supplied nothing and a stand-in was drawn.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### resolved *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

“descriptor” | “parts” | “placeholder”.
Environments: “store” | “preset” | “default”.

* **Type:**
  What was built. Characters

### *class* an.stage.serialize.AssetsJSON(\*\*data)

Bases: `_JSONModel`

Map of asset id → AssetJSON, split by kind.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.stage.serialize.BUILTIN_VISUAL_KINDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('sprite', 'rect', 'ellipse', 'svg_sprite', 'path')*

The visual kinds `runtime.js` draws itself. Any other kind must be
registered by a genre’s runtime script (an#247), or it draws as a rect.

### *class* an.stage.serialize.ChannelJSON(\*\*data)

Bases: `_JSONModel`

One animated property of one target.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.CutoutSceneJSON(\*\*data)

Bases: `_JSONModel`

Top-level cutout scene JSON — the JS runtime’s input contract.

Versioned so the runtime can refuse incompatible inputs.

#### asset_resolution *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[AssetResolutionJSON](_autosummary/an.stage.serialize.html.md#an.stage.serialize.AssetResolutionJSON)]*

One entry per drawable entity, in scene order — see
[`AssetResolutionJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.AssetResolutionJSON). Inert to the runtime; read by the bench
harness and the golden-corpus bless to assert WHICH render path ran.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### overlay *: [NodeJSON](_autosummary/an.stage.serialize.html.md#an.stage.serialize.NodeJSON) | [None](https://docs.python.org/3/builtins/constants.html#None)*

a second top-level container the
runtime centres on the canvas and never indexes, so no channel — the
camera’s `root.pivot`/`root.scale` included — can reach it. Its
children are indexed by their own paths exactly like `scene`’s, so an
overlay title’s words tween like anything else. `None` when the shot
has no overlay, and then omitted from the serialized document.

* **Type:**
  The camera-immune layer (an#155)

### *class* an.stage.serialize.CutoutSceneMetaJSON(\*\*data)

Bases: `_JSONModel`

Per-shot metadata.

#### blink_phases *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [float](https://docs.python.org/3/builtins/functions.html#float)]*

Per-entity blink phase in [0, 1), a pure function of the entity NAME
(an#88). Stamped by the compiler — which now emits blinks as channels —
and inert to the runtime. It is recorded because renaming a corpus
character silently re-phases every blink and moves every pixel metric;
a stamped phase turns that into a visible diff instead of an
unexplained metric shift. (The runtime’s determinism probe used to
carry this; the fact moved with the mechanism.)

#### entity_spaces *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Per entity, the property space its kind declares, where that is NOT the
kernel default (`stage.node`) – so evaluating this document with no
explicit space evaluates each target in its entity’s space, as
`an validate` and the compiler already do (an#245; read by
[`an.timing.timeline.timeline_from_compiled()`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.timeline_from_compiled)). \*\*Serialized only when
non-empty\*\*: no shipped kind declares another space, so no document and
no contract hash moves until a genre registers one.

#### extensions *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

What genre compile passes add to the document’s meta, by key (an#247):
the generic slot a pass writes through `CompileState.meta_extensions`
rather than a field the stage would have to know by name. Also where a
genre’s explicit replacement of a stage pass is recorded
(`replaced_compile_passes`). **Serialized only when non-empty.**

#### fonts *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

entity id ->
`"<family> <style> [(embedded)] sha256:<digest>"` — identity by the
font’s BYTES, since a family name is not one. Provenance, inert to the
runtime (the glyphs are already SVG). **Serialized only when non-empty.**

* **Type:**
  Per text block, the face that set it (an#155)

#### gaze_seeds *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [int](https://docs.python.org/3/builtins/functions.html#int)]*

Per-entity saccade seed (an#99), a pure function of the entity NAME
like `blink_phases`; stamped for the rigs that have pupils and
**serialized only when non-empty** — a pre-Wave-6 rig has no pupils and
its compiled document, the bench’s scene contract, must not move.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### spaces *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]*

The definition of each space `entity_spaces` names, in the timing
contract’s `kinds.json` form (`PropertySpace.to_json`), so the
document is self-describing: `runtime.js` has no registry and evaluates
a declared entity’s targets by THESE field kinds and write groups
(an#287), and [`an.timing.timeline.timeline_from_compiled()`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.timeline_from_compiled) reads the
same definitions back. **Serialized only when non-empty** (it is empty
exactly when `entity_spaces` is).

#### step_hz *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The stepped-timing policy the shot’s tweens were compiled under
(an#89); `None` = smooth. **Serialized only when set**: the compiled
document is the bench’s scene contract (`scene_contract_sha256`), so a
`null` here would move every committed row’s hash for a knob nobody
turned. Inert to the runtime; read by the ledger.

#### style_pack *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The name of the `StylePack` this scene was compiled under, or `None`.
Recorded so a rendered document says which art direction produced it —
the pack’s COLOURS are already resolved into the nodes, so this is
provenance rather than an instruction. Omitted from the serialized
document when unset (see the wrap serializer below).

### *class* an.stage.serialize.KeyframeJSON(\*\*data)

Bases: `_JSONModel`

Single keyframe in an animation channel.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.NodeJSON(\*\*data)

Bases: `_JSONModel`

One node in the scene tree.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### paint_order *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

The order its descendants’ visuals are PAINTED in, when it is not the
tree’s (an#430): node paths relative to this node, back to front. Set on
a rig’s entity node whose nested chain interleaves with an unrelated
part (an upper arm 1, a cord 2, a forearm 3); the runtime keeps each
visual in its transform tree and paints it from this list.

#### scope *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

as the children of the
node named `scope` in the same parent (`""`: as the parent’s own
children). `None` = under this node’s own path, the rule before
an#343. An environment’s foreground container carries `scope=<env>`,
so every plane is `<env>/<plane>` wherever the environment is cut.
[`an.stage.tree`](_autosummary/an.stage.tree.html.md#module-an.stage.tree) is the Python statement of the rule.

* **Type:**
  Where this node’s CHILDREN are indexed (an#343)

#### z_index *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

This node’s place among its siblings when its parent sorts them
(an#403): a nested chain whose declared draw order is not the tree’s
(a far arm behind the torso it nests under). `None` = insertion order.

### *class* an.stage.serialize.PathJSON(\*\*data)

Bases: `_JSONModel`

A stroked path’s drawing instruction (an#160), carried on a `path` visual.

`points` is always a POLYLINE — the compiler flattens cubic Béziers
(`an.stage.path_geometry.flatten_curve`), so the runtime knows one geometry.
`trim_start` / `trim_end` are the values shown before any channel
touches the node; channels on those two properties move them.
`head_length == 0` means no arrowhead. What the runtime draws from this
is specified by `an.stage.path_geometry.path_geometry`.

#### closed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

`closed` joins the stroke at the
first point (the polyline already returns there); `fill` (`#rrggbb`)
is drawn under the stroke at `fill_alpha` and is not trimmed. Omitted
from the wire when off, so no existing path document’s hash moves.

* **Type:**
  A closed shape and its fill (an#161)

#### dash *: [float](https://docs.python.org/3/builtins/functions.html#float)*

`dash > 0` is on. `dash_offset` is the value
shown before a channel touches it. These are wire fields of path
visuals only, so they cannot move a non-path document’s hash.

* **Type:**
  Dash pattern (an#161)

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### tail_head_length *: [float](https://docs.python.org/3/builtins/functions.html#float)*

A tail arrowhead at the trimmed start (an#161), `0` = none. Omitted
from the wire when off, so no existing path document’s hash moves.

#### width_profile *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`[[t, factor], ...]` over the whole
path’s length; the runtime fills `path_geometry`’s `outlines`.
Omitted when unset.

* **Type:**
  A variable-width stroke (an#161)

### *class* an.stage.serialize.PlacedClipJSON(\*\*data)

Bases: `_JSONModel`

An animation placed on a track at a specific time.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.TimelineJSON(\*\*data)

Bases: `_JSONModel`

Top-level timeline: total duration + tracks.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.TrackJSON(\*\*data)

Bases: `_JSONModel`

A sequence of placed clips with optional target-prefix metadata.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.TransformJSON(\*\*data)

Bases: `_JSONModel`

Local transform of a scene-graph node (authoring form).

`alpha` is not geometry, but it lives here for the same reason the rest
does: this is the per-node property bag the runtime applies, and it is
animatable through the same channel machinery.

It is set on the node’s *container*, so it **cascades** — fading a character
fades every part of it. Note that this is per-part compositing, not a
flattened group fade: where two parts of the same character overlap, the
seam is visible mid-fade. That is the standard behaviour of a 2D scene graph
and the right default; a true group fade needs the subtree rendered to a
texture first, which costs a render pass per node per frame.

\*\*This class’s field defaults are the single source of truth for a
property’s rest value\*\* — see `compile.py`’s `_PROPERTY_REST_VALUES`,
which is derived from them rather than restated.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.UnderlayJSON(\*\*data)

Bases: `_JSONModel`

A copy of a node’s own visual, drawn BEHIND it in the same container
(an#163 gap 5: the outline and the paper-gap shadow).

Being in the node’s container is the whole design: the copy takes every
transform the node takes — tweens, `play`, the camera — with no channel of
its own, and a swap on the node re-textures its copies (`runtime.js`
`applySwap`). One copy is drawn per entry of `offsets` (in the node’s own
frame), each grown by `grow` pixels: a rect/ellipse is redrawn with its
geometry grown, an SVG sprite is scaled about its art’s centre so its box
grows by `grow` on every side. `color` is the fill of a redrawn shape
and the `tint` (a multiply) of a sprite copy. Compiled by
`an.stage.surface`; never authored.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.stage.serialize.VisualJSON(\*\*data)

Bases: `_JSONModel`

Drawable content attached to a node.

`kind="svg_sprite"` is the Phase 11b path: the runtime instantiates a
`PIXI.Sprite` from a pre-loaded SVG texture identified by `asset_id`.

`asset_sets` carries this node’s swap vocabulary —
`{set_name: {KEY: asset_id}}`, the compiler’s per-slot **projection** of
the descriptor’s `asset_sets` onto the slot this visual draws (an#87).
A channel whose property names one of these sets swaps the sprite’s
texture by key; `viseme` is just the conventional set name lip-sync
uses. Same field name as `CharacterDescriptor.asset_sets` on purpose:
one vocabulary, two layers (descriptor = declared, wire = resolved to
texture aliases). Replaces the mouth-only `viseme_assets`.

`width`/`height` are the box the art is fitted **into**, not the size it
is forced to. Under `fit="contain"` the art keeps its own aspect ratio and
may leave slack on one axis; that slack is the correct rendering, not a bug.

#### asset_geometry *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [float](https://docs.python.org/3/builtins/functions.html#float)]] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Per-texture geometry for a swap key drawn differently from the built
one (an#211): `{asset_id: {"width", "height", "anchor_x", "anchor_y",
"x", "y"}}` — the box the key is fitted into, its anchor, and its
offset from the node (scene pixels). A swap used to carry the texture
only, so every key drew in the DEFAULT attachment’s box: a closed mouth
on a thin canvas squashed every open mouth to a fraction of a pixel. Only
keys whose geometry differs are listed; `None` = every key shares it.

#### blend *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['add', 'multiply'] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`"add"` for a
glow, `"multiply"` for the paper grain. PixiJS 7 does both in the
blend equation — no filter, no render texture. `None` = normal.

* **Type:**
  The engine’s native blend mode for this visual (an#163)

#### fit *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['stretch', 'contain']*

How the art is fitted to `width`/`height`.

`"contain"` scales uniformly so the art keeps the shape it was drawn
with — the invariant of an#74. `"stretch"` sizes each axis
independently, which is what every sprite did before that issue and what
distorted `arm_l` by 3.929x on the repo’s own art.

Additive with a `"stretch"` default so no stored scene changes meaning;
the compiler emits `"contain"` for every sprite it builds.

#### kind *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

One of [`BUILTIN_VISUAL_KINDS`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.BUILTIN_VISUAL_KINDS), or a kind a genre’s runtime script
registers (`window.anRegisterVisual`, an#247). A string on the wire, as
it always was; the open set is what lets `cutan` ship the mouth and eye.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### path *: [PathJSON](_autosummary/an.stage.serialize.html.md#an.stage.serialize.PathJSON) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The stroke for `kind="path"` (an#160); `None` on every other visual.

#### underlays *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[UnderlayJSON](_autosummary/an.stage.serialize.html.md#an.stage.serialize.UnderlayJSON)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Copies drawn behind this visual, back to front (an#163) — the outline
and the paper-gap shadow. Only `rect`, `ellipse` and `svg_sprite`
take them; the runtime refuses any other kind.

#### z_index *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The visual’s place among its node’s children when the node sorts them
(an#403): a part drawn over the chain nested under it.

### an.stage.serialize.from_dict(d)

Rebuild a scene from a plain-dict representation.

* **Return type:**
  [`CutoutSceneJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.CutoutSceneJSON)

### an.stage.serialize.to_dict(scene)

Dump a scene to a plain-dict representation (no None pruning).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]


# _autosummary/an.stage.snapshot.html.md

# an.stage.snapshot

Rasterise art files to PNG in the browser the stage already uses (an#347).

`an library sheet --parts` tiles an asset’s part files; most cut-out parts
are SVG, which nothing in the core can decode. Chromium draws them exactly as
the stage will. Raster parts (PNG, JPEG, WebP, GIF) are drawn the same way, so
one path serves every format the stage loads.

### Functions

| [`rasterise`](_autosummary/an.stage.snapshot.html.md#an.stage.snapshot.rasterise)(files, \*, size)   | PNG bytes of each `(path, data)` art file, fitted into a `size` square, transparent behind.   |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|

### an.stage.snapshot.rasterise(files, , size)

PNG bytes of each `(path, data)` art file, fitted into a `size` square, transparent behind.

Raises [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError) with the install hint when the stage extra is absent.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]


# _autosummary/an.stage.surface.html.md

# an.stage.surface

Surface treatments, compiled (an#163 gap 5): outline, paper-gap shadow, glow, grain.

Every treatment here is a COMPILE-TIME expansion into ordinary document
content. None is a runtime filter, and none draws anything random at render
time. `runtime.js` keeps its rule against per-frame randomness, and its
determinism probe still sees zero filters.

- **Outline and paper-gap shadow**: [`UnderlayJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.UnderlayJSON)
  entries on a part’s visual. The runtime draws each one as a copy of the
  part’s own visual, BEHIND it, in the part’s own container. So the copy takes
  every transform the part takes (tweens, `play`, the camera, a stage scale)
  and needs no channel of its own. A swap on the part (a viseme, a blink)
  re-textures its copies in the same call.
- **Glow**: one extra child node, first in the entity so it draws behind every
  part. It is a radial-gradient SVG sprite, drawn with the engine’s native
  `add` blend.
- **Grain**: one seeded noise tile, made here as a palette PNG and tiled on the
  camera-immune overlay under any text, drawn with the native `multiply`
  blend. The PNG bytes are fully determined by the seed. The deflate stream is
  written as STORED blocks by hand, because a compressor’s output is not
  stable across zlib builds (zlib-ng differs), and the texture is part of the
  scene contract.

A scene whose pack sets none of these compiles byte-identically to before.
Nothing here runs without a treatment, and the new wire fields are omitted
when unset.

### Module Attributes

| [`UNDERLAY_KINDS`](_autosummary/an.stage.surface.html.md#an.stage.surface.UNDERLAY_KINDS)   | The visual kinds a copy can be drawn for.                                                                                                                                                                            |
|-------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`GLOW_NODE`](_autosummary/an.stage.surface.html.md#an.stage.surface.GLOW_NODE)        | The glow's node name inside its entity, and the grain's on the overlay.                                                                                                                                              |
| [`GRAIN_LEVELS`](_autosummary/an.stage.surface.html.md#an.stage.surface.GRAIN_LEVELS)     | Grey levels in the grain tile. 16 = a 4-bit palette PNG (half the bytes of an 8-bit one), and finer steps than 8-bit output could show at the small <br/><br/>```<br/>`<br/>```<br/><br/>amount\`s grain is used at. |

### Functions

| [`ring_offsets`](_autosummary/an.stage.surface.html.md#an.stage.surface.ring_offsets)(radius)                               | The outline ring's copy offsets at `radius` pixels.                                                                    |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| [`apply_surface`](_autosummary/an.stage.surface.html.md#an.stage.surface.apply_surface)(node, surface, \*, textures)         | Expand `surface` into `node` (an entity's subtree), in place.                                                          |
| [`drawn_box`](_autosummary/an.stage.surface.html.md#an.stage.surface.drawn_box)(node)                                    | `(x0, y0, x1, y1)` of what `node`'s parts draw, in its own frame.                                                      |
| [`glow_svg`](_autosummary/an.stage.surface.html.md#an.stage.surface.glow_svg)(width, height, \*, color, intensity, ...) | The glow's texture: an elliptical radial gradient filling its box.                                                     |
| [`grain_indices`](_autosummary/an.stage.surface.html.md#an.stage.surface.grain_indices)(seed, tile)                          | `tile × tile` grey-level indices in `[0, GRAIN_LEVELS)`, row-major.                                                    |
| [`grain_greys`](_autosummary/an.stage.surface.html.md#an.stage.surface.grain_greys)(amount)                                | The palette: level `n` multiplies the frame by `grey/255`, from white (level 0) down to `1 − amount` (the last level). |
| [`grain_png`](_autosummary/an.stage.surface.html.md#an.stage.surface.grain_png)(\*, seed, amount, tile)                  | The grain tile as a 4-bit palette PNG: opaque and lossless.                                                            |
| [`grain_node`](_autosummary/an.stage.surface.html.md#an.stage.surface.grain_node)(grain, \*, width, height, textures)     | The grain layer: one tile texture, tiled over the frame in frame pixels.                                               |
| [`faded_treated_targets`](_autosummary/an.stage.surface.html.md#an.stage.surface.faded_treated_targets)(scene, animations)           | The `alpha` channel targets that FADE a part carrying underlays.                                                       |

### an.stage.surface.GLOW_NODE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '_glow'*

The glow’s node name inside its entity, and the grain’s on the overlay.
Leading underscore: no rig slot or entity id is spelled like this, and a
collision with an overlay entity still raises in `compile_shot`.

### an.stage.surface.GRAIN_LEVELS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 16*

Grey levels in the grain tile. 16 = a 4-bit palette PNG (half the bytes
of an 8-bit one), and finer steps than 8-bit output could show at the small

```
`
```

amount\`s grain is used at.

### an.stage.surface.UNDERLAY_KINDS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'ellipse', 'rect', 'svg_sprite'})*

The visual kinds a copy can be drawn for. `runtime.js` refuses any other.
An eye already draws its own rim, a procedural mouth is a redraw function,
and a path is already a line.

### an.stage.surface.apply_surface(node, surface, , textures)

Expand `surface` into `node` (an entity’s subtree), in place.

Returns what it could not do, for the compiler to warn with (a glow on an
entity that draws nothing). A no-op for `None`, which is every entity of
a scene whose pack sets no treatment. The outline and the shadow go on each part: the entity’s
direct children, and deeper parts too when the treatment is `nested`.
The glow is added after the parts are walked, so it never gets an outline
of its own.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> from an.styles import SurfaceTreatment
>>> part = NodeJSON(name="torso", visual=VisualJSON(kind="rect", width=40, height=60))
>>> ent = NodeJSON(name="bob", children=[part])
>>> apply_surface(ent, SurfaceTreatment(outline={"width": 2}, shadow={}), textures={})
[]
>>> [(u.grow, u.offsets) for u in part.visual.underlays]
[(2.0, [(4.0, 4.0)]), (2.0, [(0.0, 0.0)])]
```

### an.stage.surface.drawn_box(node)

`(x0, y0, x1, y1)` of what `node`’s parts draw, in its own frame.

The entity’s own visual and every descendant’s: rest translations and each
visual’s box and anchor. Rest rotations and
scales are not applied: the compiled rigs have none below the entity
root. That is an assumption about the rigs this compiler builds, not
about arbitrary documents.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> head = NodeJSON(name="h", transform=TransformJSON(y=-50),
...                 visual=VisualJSON(kind="ellipse", width=40, height=40))
>>> drawn_box(NodeJSON(name="e", children=[head]))
(-20.0, -70.0, 20.0, -30.0)
```

### an.stage.surface.faded_treated_targets(scene, animations)

The `alpha` channel targets that FADE a part carrying underlays.

A treated part’s copies are drawn separately, so a fade shows them
through the part instead of the background (see `an.styles.Outline`).
A hide or a show is not a fade (`_fades()`), and an alpha on the glow
node only fades the glow, which is fine.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.stage.surface.glow_svg(width, height, , color, intensity, core)

The glow’s texture: an elliptical radial gradient filling its box.

It holds `intensity` out to `core` (a fraction of the radius) and
fades to nothing at the edge.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> glow_svg(10, 10, color="#ffffff", intensity=0.5, core=0.4)[:52]
'<svg xmlns="http://www.w3.org/2000/svg" width="10" h'
```

### an.stage.surface.grain_greys(amount)

The palette: level `n` multiplies the frame by `grey/255`, from
white (level 0) down to `1 − amount` (the last level).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> grain_greys(0.06)[:3], grain_greys(0.06)[-1]
([255, 254, 253], 240)
```

### an.stage.surface.grain_indices(seed, tile)

`tile × tile` grey-level indices in `[0, GRAIN_LEVELS)`, row-major.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> grain_indices(0, 4)
[14, 6, 0, 15, 1, 5, 2, 12, 3, 15, 6, 12, 8, 8, 11, 8]
>>> grain_indices(0, 4) == grain_indices(0, 4) != grain_indices(1, 4)
True
```

### an.stage.surface.grain_node(grain, , width, height, textures)

The grain layer: one tile texture, tiled over the frame in frame pixels.

It goes on the OVERLAY, which is centred on the canvas and cannot be
reached by the camera. Each tile sits at an integer frame position at
scale 1, so at `supersample` 1 a texel is exactly one pixel. Tiling is
done with nodes rather than a `TilingSprite`, which the runtime does not
wire (an#110’s rule: wire it fully or not at all).

* **Return type:**
  [`NodeJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.NodeJSON)

### an.stage.surface.grain_png(, seed, amount, tile)

The grain tile as a 4-bit palette PNG: opaque and lossless.

It is opaque on purpose. With no alpha channel there is no premultiply
step on load that could differ between engines.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### an.stage.surface.ring_offsets(radius)

The outline ring’s copy offsets at `radius` pixels.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

```pycon
>>> ring_offsets(2.0)[:3]
[(2.0, 0.0), (1.414214, 1.414214), (0.0, 2.0)]
>>> len(ring_offsets(3.0)) == OUTLINE_RING
True
```


# _autosummary/an.stage.text.html.md

# an.stage.text

Words on screen: title cards, labels, and text you can animate word by word.

Epic #9 Wave 8, first slice; the design question was an#155. A text block is a
**prop** whose document `kind` is `TextDescriptor` — the shape stroked paths
took ([`an.stage.paths`](_autosummary/an.stage.paths.html.md#module-an.stage.paths)) — so a scene names it with an ordinary
`AssetRef(kind="prop", ...)`, its `overrides` supply the per-shot string,
and nothing in the scene IR changed (no field, no migration).

```pycon
>>> title = TextDescriptor(name="title", text="Words on screen", layer="overlay")
>>> title.kind, title.unit, title.layer
('TextDescriptor', 'word', 'overlay')
```

**Typesetting is not done here.** The sibling package `tituli` owns it —
shaping, real glyph metrics, wrapping, alignment, the title-safe area — and
emits a `Layout` of placed `Run``s, one per unit. :func:`layout_text` asks it
for that layout and for each run's glyph contours (``tituli.run_outline`), and
the compiler turns each unit into one node whose visual is an SVG sprite
converted at compile time. The runtime never learns what text is, so fonts
never reach the frame path and the determinism perimeter is unchanged.

**Units are addressable, so text animates with ordinary tweens.** A block of
`unit="word"` builds `<id>/word_0`, `<id>/word_1`, …; `"glyph"` and
`"line"` likewise. `index` counts DRAWN units in reading order (spaces are
not units). [`reveal_units()`](_autosummary/an.stage.text.html.md#an.stage.text.reveal_units) is a Python-side generator of ordinary actions
for a staggered reveal — a preset, not a new IR node (the general combinator is
[`an.ir.compose.stagger()`](_autosummary/an.ir.compose.html.md#an.ir.compose.stagger)):

```pycon
>>> from an.ir.compose import flatten
>>> reveal = reveal_units("title", 3, "alpha", to=1.0, from_=0.0, duration=0.3, step=0.1)
>>> [(f.action.target, round(f.start, 3)) for a in reveal for f in flatten(a)
...  if f.action.kind == "tween"]
[('title/word_0', 0.0), ('title/word_1', 0.1), ('title/word_2', 0.2)]
```

**Two layers.** `layer="world"` is in the scene and moves with the camera, like
any prop. `layer="overlay"` is drawn in a second top-level container the
camera never touches — a title card stays exactly where it is through a
push-in while an in-world label grows with the scene.

**Fonts fail loudly and never depend on the machine.** `font=None` is the
face Pillow embeds (Aileron, CC0 — “No Rights Reserved”), requested through
`tituli.EMBEDDED` so the installed fonts are never consulted: the same bytes
on every machine with the same Pillow, and no typeface is vendored in this
package. `font` otherwise names a font FILE (`.ttf`/`.otf`/`.ttc`),
absolute or relative to the text document’s directory in the props store;
a path that is not a file, or a file the typesetter could not use, RAISES
([`TextFontError`](_autosummary/an.stage.text.html.md#an.stage.text.TextFontError)). A family NAME is refused on purpose: resolving one
would make the picture depend on what a machine has installed. A character
the face has no glyph for raises too — `an` is English-first and refuses
loudly rather than drawing a box. The face’s identity (sha256 of its bytes) is
recorded in the compiled document.

### Module Attributes

| [`TEXT_DOCUMENT_KIND`](_autosummary/an.stage.text.html.md#an.stage.text.TEXT_DOCUMENT_KIND)   | Its own versioned document kind, registered from the module that owns the schema (the rule `PathDescriptor` and `PropDescriptor` follow).                        |
|-----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DFLT_TEXT_SIZE`](_autosummary/an.stage.text.html.md#an.stage.text.DFLT_TEXT_SIZE)       | 0.06 is 65 px at 1080p, and the same block reads the same at 720p and at 4K.                                                                                     |
| [`DFLT_TEXT_COLOUR`](_autosummary/an.stage.text.html.md#an.stage.text.DFLT_TEXT_COLOUR)     | Ink when the document names none — a near-black that reads on the default white background.                                                                      |
| [`RESERVED_TEXT_IDS`](_autosummary/an.stage.text.html.md#an.stage.text.RESERVED_TEXT_IDS)    | the runtime indexes the scene's container as `root` (the camera's target), and the overlay container is named `overlay`.                                         |
| [`BLOCK_UNIT`](_autosummary/an.stage.text.html.md#an.stage.text.BLOCK_UNIT)           | The unit that typesets the whole string as ONE node, `block_0` (an#341): what a replacement set swaps, since per-word units of different strings do not line up. |
| [`TEXT_SET`](_autosummary/an.stage.text.html.md#an.stage.text.TEXT_SET)             | The swap set a text block with `texts` declares (an#341).                                                                                                        |

### Functions

| [`resolve_text`](_autosummary/an.stage.text.html.md#an.stage.text.resolve_text)(document[, overrides])               | The text block an entity draws: its stored document with `overrides` on top.                                                                                                                                                                                                              |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`font_base_dir`](_autosummary/an.stage.text.html.md#an.stage.text.font_base_dir)(props_store, ref)                   | What a relative `font` path resolves against: the text document's own directory in an on-disk props store — `None` for an in-memory one, where a relative path then RAISES rather than resolving against the working directory (which would make the picture depend on where you ran it). |
| [`text_entity_problem`](_autosummary/an.stage.text.html.md#an.stage.text.text_entity_problem)(entity, desc)                 | What is wrong with WHERE this entity puts its block, or `None`.                                                                                                                                                                                                                           |
| [`layout_text`](_autosummary/an.stage.text.html.md#an.stage.text.layout_text)(desc, \*, width, height[, base_dir])  | Set `desc` on a `width` x `height` frame and take each unit's contours.                                                                                                                                                                                                                   |
| [`unit_names`](_autosummary/an.stage.text.html.md#an.stage.text.unit_names)(desc, \*, width, height[, base_dir])   | The node names a block builds — what `<id>/<name>` targets may address.                                                                                                                                                                                                                   |
| [`layout_text_set`](_autosummary/an.stage.text.html.md#an.stage.text.layout_text_set)(desc, \*, width, height[, ...])   | Every string of a block's replacement set, set once: `{key: layout}`.                                                                                                                                                                                                                     |
| [`text_set_keys`](_autosummary/an.stage.text.html.md#an.stage.text.text_set_keys)(desc)                               | `{key: key}` for a block's replacement set — the `text` set as a swap declaration states it (`{}` for a single-string block).                                                                                                                                                             |
| [`reveal_units`](_autosummary/an.stage.text.html.md#an.stage.text.reveal_units)(entity_id, count, property, \*, ...) | Tween `property` from `from_` to `to` on units `0..count-1` of a text block, each `step` seconds after the last — a word-by-word (or letter-by-letter) reveal.                                                                                                                            |

### Classes

| [`TextDescriptor`](_autosummary/an.stage.text.html.md#an.stage.text.TextDescriptor)(\*\*data)                      | The on-disk text schema, saved as a prop's `prop.json`.                                                                                                                                                                                                                |
|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`Counter`](_autosummary/an.stage.text.html.md#an.stage.text.Counter)(\*\*data)                             | A counter block's number-to-text rule (an#342): `format` in the declared d3-format subset ([`an.formats`](_autosummary/an.formats.html.md#module-an.formats)), and the value shown before any `value` action (`start`; `None` = the first action's value). |
| [`TextUnit`](_autosummary/an.stage.text.html.md#an.stage.text.TextUnit)(name, text, box, d)                  | One addressable unit: its node name, its string, its box and its ink.                                                                                                                                                                                                  |
| [`TextLayout`](_autosummary/an.stage.text.html.md#an.stage.text.TextLayout)(units, origin, font[, bounds])     | A placed block: its units, its reference point, and the face that set it.                                                                                                                                                                                              |
| [`FontIdentity`](_autosummary/an.stage.text.html.md#an.stage.text.FontIdentity)(family, style, sha256, embedded) | Which face drew a block — by its bytes, not its name.                                                                                                                                                                                                                  |

### Exceptions

| [`TextFontError`](_autosummary/an.stage.text.html.md#an.stage.text.TextFontError)      | A text block's font cannot be used: not a file, not a font, or not the face the typesetter actually used.   |
|---------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| [`TextLayoutError`](_autosummary/an.stage.text.html.md#an.stage.text.TextLayoutError)    | The text cannot be set as asked (a glyph the face lacks, nothing to draw).                                  |
| [`TextOutlineWarning`](_autosummary/an.stage.text.html.md#an.stage.text.TextOutlineWarning) | A unit's outline is wide enough to cover its neighbour's glyphs.                                            |

### an.stage.text.BLOCK_UNIT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'block'*

The unit that typesets the whole string as ONE node, `block_0` (an#341):
what a replacement set swaps, since per-word units of different strings do
not line up.

### *class* an.stage.text.Counter(\*\*data)

Bases: `BaseModel`

A counter block’s number-to-text rule (an#342): `format` in the
declared d3-format subset ([`an.formats`](_autosummary/an.formats.html.md#module-an.formats)), and the value shown before
any `value` action (`start`; `None` = the first action’s value).

```pycon
>>> Counter(format="Day {d}", start=1).format
'Day {d}'
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### show(value)

`value` as this counter draws it.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.stage.text.DFLT_TEXT_COLOUR *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '#1a1a1a'*

Ink when the document names none — a near-black that reads on the default
white background.

### an.stage.text.DFLT_TEXT_SIZE *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.06*

0.06 is
65 px at 1080p, and the same block reads the same at 720p and at 4K.

* **Type:**
  Type size as a FRACTION OF FRAME HEIGHT (tituli’s convention)

### *class* an.stage.text.FontIdentity(family, style, sha256, embedded, layout_engine='basic', features=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Which face drew a block — by its bytes, not its name.

#### features *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ()*

a tag
asked for and missing here was not in the font. Labelled only when
there is one, so a block without features labels as before.

* **Type:**
  The requested OpenType features this face applied (an#362)

#### label()

`"Aileron Regular (embedded) sha256:6985… layout:basic"` — what the
compiled document records.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### layout_engine *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'basic'*

Pillow’s layout engine for this face (`basic` or `raqm`). Recorded
because it moves glyph ADVANCES: RAQM (HarfBuzz) applies kerning and
ligatures and is used for a font file whenever libraqm can be loaded,
so the same bytes can set differently on two machines. The embedded
face is always `basic`.

### an.stage.text.RESERVED_TEXT_IDS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'overlay', 'root'})*

the runtime indexes the scene’s
container as `root` (the camera’s target), and the overlay container is
named `overlay`. An overlay block called `root` would overwrite the
camera’s node in the runtime’s index and take the push-in with it.

* **Type:**
  Entity ids a text block may not take

### an.stage.text.TEXT_DOCUMENT_KIND *: [DocumentKind](_autosummary/an.ir.html.md#an.ir.DocumentKind)* *= DocumentKind(name='TextDescriptor', version_field='schema_version', current_version='0.1.0')*

Its own versioned document kind, registered from the module that owns the
schema (the rule `PathDescriptor` and `PropDescriptor` follow).

### an.stage.text.TEXT_SET *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'text'*

The swap set a text block with `texts` declares (an#341).

### *class* an.stage.text.TextDescriptor(\*\*data)

Bases: `BaseModel`

The on-disk text schema, saved as a prop’s `prop.json`.

`extra="forbid"` (the `PathDescriptor` precedent): a text block is a
precise instruction, and a misspelt `colour` that silently did nothing is
the defect class this package refuses. Set-but-inert combinations raise too:

```pycon
>>> TextDescriptor(name="t", text="hi", anchor="top")
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for TextDescriptor
  Value error, `anchor` places a block in the title-safe area of the FRAME, which only an overlay has; a world-layer block is placed with the entity's `stage.at` [type=value_error, input_value=..., input_type=dict]
...
```

#### anchor *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

one of tituli’s nine anchors (`"top"`, `"bottom-left"`,
`"center"`, …) inside the title-safe area. `None` centres the block
on the node origin (the frame centre, or `stage.at`).

* **Type:**
  Overlay only

#### color *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`#rrggbb`.

#### *property* content *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

its `start`, else
zero, until the compiler lowers its `value` channel).

* **Type:**
  The string the block draws at rest (a counter

#### counter *: [Counter](_autosummary/an.stage.text.html.md#an.stage.text.Counter) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`tween <id> value a→b` and
`set <id> value v` are lowered at compile to a replacement set of the
strings the frames show. Requires `unit="block"`; no `text`/`texts`.

* **Type:**
  A number that reads as text (an#342)

#### features *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`["tnum"]` sets tabular
figures. `None` = the block’s default: a `counter` asks for
`COUNTER_FEATURES`, anything else for none. Whether the face
has them is recorded with it (`meta.fonts`, `features:`).

* **Type:**
  OpenType features to apply, by tag (an#362)

#### font *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`None` = the embedded face; else a font FILE path (see the module doc).

#### max_width *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Wrap width as a fraction of frame WIDTH; `None` = break only at newlines.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### rest *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

the first key.

* **Type:**
  The key drawn when nothing has been set; default

#### *property* rest_key *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The key a block with `texts` shows at rest (`rest`, else the
first key); `None` for a single-string block.

#### size *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Fraction of frame height.

#### stroke_color *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The outline’s `#rrggbb`; needs `stroke_width`.

#### stroke_width *: [float](https://docs.python.org/3/builtins/functions.html#float)*

An outline around every glyph (an#313, OverSimplified’s white labels
edged in black): its visible thickness OUTSIDE the glyph, in scene
pixels, drawn under the fill in the same texture, so a per-unit
reveal shows outline and fill together. `0` = none.

#### text *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The words. Explicit newlines break lines; `max_width` wraps. Exactly
one of `text` and `texts` is given.

#### texts *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`{key: string}`, every string one drawing
of the block, swapped by `set <id> text <key>` — replacement animation
applied to text. Keys obey the swap-key rules (no `/`, no `::`).
Requires `unit="block"`.

* **Type:**
  A replacement set (an#341)

#### tracking *: [float](https://docs.python.org/3/builtins/functions.html#float)*

tituli sets a
tracked string one run per glyph, so a word unit would not exist.

* **Type:**
  Extra advance per glyph, in em. Only with `unit="glyph"`

#### *property* typeset_features *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

its own, else a counter’s
tabular figures (an#362), so a number does not change width as it counts.

```pycon
>>> TextDescriptor(name="n", counter={"format": "{d}"}, unit="block").typeset_features
('tnum',)
>>> TextDescriptor(name="t", text="x").typeset_features
()
```

* **Type:**
  The features the block is set with

#### unit *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['word', 'glyph', 'line', 'block']*

a word, a glyph, a whole line, or the
whole block (`block_0`, one node however many lines).

* **Type:**
  What one addressable node is

### *exception* an.stage.text.TextFontError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A text block’s font cannot be used: not a file, not a font, or not the
face the typesetter actually used. Raised instead of falling back, because
a fallback face is a different picture wearing the right one’s clothes.

### *class* an.stage.text.TextLayout(units, origin, font, bounds=(0.0, 0.0, 0.0, 0.0))

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A placed block: its units, its reference point, and the face that set it.

`origin` is the block’s reference point in frame pixels — the frame
centre, or its title-safe anchor position — which is where the block’s own
node sits; each unit’s node is placed relative to it.

#### bounds *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]* *= (0.0, 0.0, 0.0, 0.0)*

advances
and line heights, not ink, so two strings of one face share a baseline
and their `align` edges can be lined up (an#341).

* **Type:**
  The block’s LAYOUT box `(x0, y0, x1, y1)` in frame pixels

### *exception* an.stage.text.TextLayoutError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

The text cannot be set as asked (a glyph the face lacks, nothing to draw).

### *exception* an.stage.text.TextOutlineWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A unit’s outline is wide enough to cover its neighbour’s glyphs.

### *class* an.stage.text.TextUnit(name, text, box, d)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One addressable unit: its node name, its string, its box and its ink.

`box` is `(x0, y0, x1, y1)` in frame pixels, snapped OUTWARD to whole
pixels and containing both the layout box and the ink, so the sprite’s
corners sit on the pixel grid and nothing is clipped. `d` is the unit’s
glyph contours as SVG path data in frame pixels.

#### *property* center *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]*

The box centre in frame pixels — where the unit’s node sits.

#### *property* size *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), [int](https://docs.python.org/3/builtins/functions.html#int)]*

`(width, height)` of `box`.

### an.stage.text.font_base_dir(props_store, ref)

What a relative `font` path resolves against: the text document’s own
directory in an on-disk props store — `None` for an in-memory one, where a
relative path then RAISES rather than resolving against the working
directory (which would make the picture depend on where you ran it).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.stage.text.layout_text(desc, , width, height, base_dir=None)

Set `desc` on a `width` x `height` frame and take each unit’s contours.

`base_dir` is what a relative `font` path resolves against — the text
document’s own directory in the props store. A block with `texts` is set
with its rest string ([`layout_text_set()`](_autosummary/an.stage.text.html.md#an.stage.text.layout_text_set) sets every one).

* **Return type:**
  [`TextLayout`](_autosummary/an.stage.text.html.md#an.stage.text.TextLayout)

```pycon
>>> lay = layout_text(TextDescriptor(name="t", text="Hello big world"), width=1920, height=1080)
>>> [u.name for u in lay.units]
['word_0', 'word_1', 'word_2']
>>> lay.origin
(960.0, 540.0)
>>> lay.font.family, lay.font.embedded
('Aileron', True)
>>> [u.name for u in layout_text(TextDescriptor(name="t", text="two\nlines", unit="block"), width=1920, height=1080).units]
['block_0']
```

### an.stage.text.layout_text_set(desc, , width, height, base_dir=None)

Every string of a block’s replacement set, set once: `{key: layout}`.

Each is set exactly as a single-string block would be, so each is centred
(or anchored) on its own; lining their `align` edges up is the
builder’s job ([`an.stage.text_layout.build_text_subtree()`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.build_text_subtree)). A block
with no `texts` returns `{}`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`TextLayout`](_autosummary/an.stage.text.html.md#an.stage.text.TextLayout)]

```pycon
>>> lays = layout_text_set(TextDescriptor(name="t", texts={"a": "1", "b": "22"}, unit="block"), width=320, height=240)
>>> sorted(lays), [lay.units[0].name for lay in lays.values()]
(['a', 'b'], ['block_0', 'block_0'])
```

### an.stage.text.resolve_text(document, overrides=None)

The text block an entity draws: its stored document with `overrides` on top.

Validated strictly AFTER the merge, so a stored document may be a reusable
style with no `text` of its own, and each entity supplies the words:

* **Return type:**
  [`TextDescriptor`](_autosummary/an.stage.text.html.md#an.stage.text.TextDescriptor)

```pycon
>>> style = {"kind": "TextDescriptor", "name": "label", "size": 0.04}
>>> resolve_text(style, {"text": "Paris"}).text
'Paris'
```

### an.stage.text.reveal_units(entity_id, count, property, , to, from_, duration, step, start=0.0, unit='word', easing='ease_out')

Tween `property` from `from_` to `to` on units `0..count-1` of a
text block, each `step` seconds after the last — a word-by-word (or
letter-by-letter) reveal. Unit `i` starts at `start + i*step`.

A unit whose tween starts later also gets a `set` to `from_` at 0, which
HOLDS until its tween begins (the compiler’s set-hold rule) — without it the
word would show its built value until its turn and then snap to `from_`.
That is also why `from_` is required.

Returns a LIST of top-level actions — `set`, a bare `tween`, or the
`sequence(delay(start), tween)` wrapper the `scene.md` parser itself
produces for a `start:` key — so `shot.actions.extend(stagger(...))`
round-trips through `scene.md` in its short form, one entry per unit.

Named `stagger` until an#241 gave the core a general combinator of that
name ([`an.ir.compose.stagger()`](_autosummary/an.ir.compose.html.md#an.ir.compose.stagger), any actions, one `parallel`);
`an.stage.text.stagger` stays as an alias of this function so old imports
keep working.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

```pycon
>>> [a.kind for a in reveal_units("t", 2, "alpha", to=1, from_=0, duration=0.2, step=0.1)]
['tween', 'set', 'sequence']
```

### an.stage.text.text_entity_problem(entity, desc)

What is wrong with WHERE this entity puts its block, or `None`.

The one statement of the placement rules, called by the compiler (which
raises) and by `an validate` (which reports) so the two agree:

- a reserved id ([`RESERVED_TEXT_IDS`](_autosummary/an.stage.text.html.md#an.stage.text.RESERVED_TEXT_IDS)) — `root` would hijack the
  camera’s node in the runtime’s index;
- `anchor` and `stage.at` together — two answers to one question.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.stage.text.text_set_keys(desc)

`{key: key}` for a block’s replacement set — the `text` set as a
swap declaration states it (`{}` for a single-string block).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> text_set_keys(TextDescriptor(name="t", texts={"a": "1", "b": "2"}, unit="block"))
{'a': 'a', 'b': 'b'}
```

### an.stage.text.unit_names(desc, , width, height, base_dir=None)

The node names a block builds — what `<id>/<name>` targets may address.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/an.stage.text_layout.html.md

# an.stage.text_layout

A text block, compiled: one node per unit, each an SVG sprite (an#155).

[`an.stage.text.layout_text()`](_autosummary/an.stage.text.html.md#an.stage.text.layout_text) asks tituli where every unit goes and what its
glyphs look like; this module turns that into the wire. The block is one
node (the entity), each unit a child named `word_<i>` / `glyph_<i>` /
`line_<i>` whose visual is an `svg_sprite` — the kind the runtime already
draws — backed by a texture whose `src` is a `data:` URI holding that
unit’s contours. So:

- the runtime gains no visual kind and never rasterises a font (option 2 of
  an#155); text reaches the frame path as SVG art, exactly as rig parts do;
- the compiled document is self-contained — no staging, no file to lose — and
  the scene contract hash covers the glyphs themselves, so a different face
  moves it;
- each unit node sits at its box centre with the sprite anchored at 0.5, so a
  `scale_x`/`scale_y` pop or a `rotation` pivots about the unit’s middle;
- a block with a replacement set (`texts`, an#341) is one `block_0` node
  whose visual carries a `text` swap set, one texture per string and a box
  per string on the `align` edge — replacement animation, which the runtime
  already draws (`applySwap`).

**Crispness.** The SVG declares `TEXT_TEXTURE_OVERSAMPLE` x its box as its
intrinsic size, and the sprite is fitted back into the box (`fit="contain"`),
so a texel is half a scene pixel: an overlay at 1:1 samples it as a clean 2:1
box filter, and a world label survives a push-in. Every box is snapped outward
to whole pixels, so at zoom 1 a sprite’s corners sit on the pixel grid.

### Module Attributes

| [`TEXT_TEXTURE_OVERSAMPLE`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.TEXT_TEXTURE_OVERSAMPLE)   | Texels per scene pixel in a unit's texture.                                |
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`TEXT_TEXTURE_PREFIX`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.TEXT_TEXTURE_PREFIX)       | What a text texture's alias starts with — `text.<entity>.<unit>.<digest>`. |
| [`TEXT_ALIAS_DIGEST_LEN`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.TEXT_ALIAS_DIGEST_LEN)     | Hex digits of the texture's sha256 kept in its alias.                      |
| [`INLINE_SRC_PREFIX`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.INLINE_SRC_PREFIX)         | The `src` scheme of a texture that carries its bytes inline.               |

### Functions

| [`build_text_subtree`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.build_text_subtree)(entity, document, \*, ...)    | The block's node and its unit children, plus what it resolved to.                                                                                                                               |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`svg_data_uri`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.svg_data_uri)(svg)                                | `data:image/svg+xml;base64,…` — the form the vendored engine's SVG loader recognises by prefix.                                                                                                 |
| [`text_document`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.text_document)(entity, props_store)               | The stored document behind a prop entity if it is a text block, else None.                                                                                                                      |
| [`text_swap_declaration`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.text_swap_declaration)(entity, mall)              | The core `prop` kind's swap declaration (an#341): a text block with `texts` declares its `text` set, `{key: key}`; any other prop declares nothing (its built nodes' sets are its declaration). |
| [`unit_svg`](_autosummary/an.stage.text_layout.html.md#an.stage.text_layout.unit_svg)(d, box, \*, color[, stroke_width, ...]) | One unit's texture: its contours in a viewBox equal to its frame-pixel box.                                                                                                                     |

### an.stage.text_layout.INLINE_SRC_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'data:'*

The `src` scheme of a texture that carries its bytes inline. The staging
step skips it (there is nothing to copy) instead of warning that the prefix
names no store.

### an.stage.text_layout.TEXT_ALIAS_DIGEST_LEN *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 12*

Hex digits of the texture’s sha256 kept in its alias.

### an.stage.text_layout.TEXT_TEXTURE_OVERSAMPLE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 2*

Texels per scene pixel in a unit’s texture. See the module docstring.

### an.stage.text_layout.TEXT_TEXTURE_PREFIX *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'text.'*

What a text texture’s alias starts with — `text.<entity>.<unit>.<digest>`.

### an.stage.text_layout.build_text_subtree(entity, document, , width, height, base_dir, textures, resolutions)

The block’s node and its unit children, plus what it resolved to.

Raises `ValueError` subclasses (`TextFontError`, `TextLayoutError`,
pydantic’s `ValidationError`) — the compiler wraps them.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`NodeJSON`](_autosummary/an.stage.serialize.html.md#an.stage.serialize.NodeJSON), [`TextDescriptor`](_autosummary/an.stage.text.html.md#an.stage.text.TextDescriptor), [`TextLayout`](_autosummary/an.stage.text.html.md#an.stage.text.TextLayout)]

### an.stage.text_layout.svg_data_uri(svg)

`data:image/svg+xml;base64,…` — the form the vendored engine’s SVG
loader recognises by prefix.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.stage.text_layout.text_document(entity, props_store)

The stored document behind a prop entity if it is a text block, else None.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.stage.text_layout.text_swap_declaration(entity, mall)

The core `prop` kind’s swap declaration (an#341): a text block with
`texts` declares its `text` set, `{key: key}`; any other prop
declares nothing (its built nodes’ sets are its declaration).

Through [`an.genres.EntityKind.swap_declaration`](_autosummary/an.genres.html.md#an.genres.EntityKind.swap_declaration), so the compiler’s
swap vocabulary, its swap checks and `an validate` learn the set the way
they learn a rig’s. `descriptor` stays `None`: the text block has no
lowering, and a descriptor is what a genre’s passes read as a rig. A
document that does not resolve declares nothing here; the builder raises
on it, naming why.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.stage.text_layout.unit_svg(d, box, , color, stroke_width=0.0, stroke_color=None)

One unit’s texture: its contours in a viewBox equal to its frame-pixel box.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> unit_svg("M0 0L2 0L2 2Z", (0, 0, 4, 4), color="#123456")[:60]
'<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"'
```

With an outline (an#313) the same contours are drawn first as a stroke
TWICE `stroke_width` wide, round-joined, so `stroke_width` shows outside
the glyph once the fill covers the inner half: two paths rather than
`paint-order`, so no rasteriser’s support for that attribute is assumed.

```pycon
>>> 'stroke-width="6"' in unit_svg("M0 0Z", (0, 0, 4, 4), color="#fff", stroke_width=3, stroke_color="#000")
True
```


# _autosummary/an.stage.timeline.html.md

# an.stage.timeline

Stage timeline helpers: the compiled scene as a `Timeline`, and screen space.

The evaluation itself — tracks of placed clips, write groups, the pure pose —
moved to [`an.timing.timeline`](_autosummary/an.timing.timeline.html.md#module-an.timing.timeline) (the timing kernel). This module re-exports
it so every existing caller keeps its import path, and keeps what is the STAGE’s
own: reading a compiled `CutoutSceneJSON` ([`timeline_from_scene()`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.timeline_from_scene)) and
composing node transforms into canvas positions ([`screen_position()`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.screen_position)).

```pycon
>>> from an.timing.channel import Channel, Keyframe
>>> from an.adapters.cutout.clip import Clip
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> clip = Clip("walk", duration=1.0, channels=[ch])
>>> tl = Timeline(duration=2.0, tracks=[Track("a", clips=[PlacedClip(clip, start_time=0.5)])])
>>> evaluate_timeline(tl, 1.0)[("a", "x")]
5.0
```

### Functions

| [`merge_poses`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.merge_poses)(\*poses)                            | Merge multiple poses with **override semantics** (later wins per key).                                                    |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| [`write_group`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.write_group)(prop)                               | What `prop` writes on a STAGE node (the `stage.node` space's groups).                                                     |
| [`evaluate_timeline`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.evaluate_timeline)(timeline, t, \*[, space])     | Evaluate `timeline` at time `t`, merging poses across tracks/clips.                                                       |
| [`clip_from_json`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.clip_from_json)(anim, \*[, name])                | One compiled animation (`compiled.schema.json`'s `animation`) as a [`Clip`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Clip). |
| [`timeline_from_scene`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.timeline_from_scene)(scene)                      | The compiled scene's `timeline`/`animations` as an evaluable `Timeline`.                                                  |
| [`transform_of`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.transform_of)(node[, pose, frame_height])        | A node's transform, with `pose` overriding what the document declares.                                                    |
| [`screen_position`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.screen_position)(scene, path, \*[, pose, point]) | Where `point` in `path`'s local space lands on the canvas.                                                                |
| [`node_box`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.node_box)(scene, path, \*[, pose])               | The canvas rectangle `(x0, y0, x1, y1)` the visual at `path` occupies (an#445).                                           |

### Classes

| [`Channel`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Channel)(target, property[, keyframes])        | Sorted keyframes for one property of one target.                        |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`Keyframe`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Keyframe)(time, value[, easing])               | One keyframe: time, value, optional per-segment easing.                 |
| [`Clip`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Clip)(name, duration[, channels, loop_mode])   | Named animation: a duration + a bundle of channels.                     |
| [`LoopMode`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.LoopMode)(\*values)                            | How a clip behaves past its natural duration.                           |
| [`PlacedClip`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.PlacedClip)(clip[, start_time, duration, ...]) | A clip placed at an absolute time on a track.                           |
| [`Track`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Track)([target_root, clips])                   | A sequence of placed clips that share a common purpose / target prefix. |
| [`Timeline`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Timeline)(duration[, tracks, space])           | A duration + ordered list of tracks.                                    |
| [`Transform2D`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Transform2D)([x, y, rotation, scale_x, ...])   | One node's local transform, in the runtime's own vocabulary.            |

### *class* an.stage.timeline.Channel(target, property, keyframes=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Sorted keyframes for one property of one target.

Construction validates that `keyframes` is non-empty and sorted.

### *class* an.stage.timeline.Clip(name, duration, channels=<factory>, loop_mode=LoopMode.ONCE)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Named animation: a duration + a bundle of channels.

### *class* an.stage.timeline.Keyframe(time, value, easing=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One keyframe: time, value, optional per-segment easing.

The easing on a keyframe describes the curve **leaving** that keyframe
toward the next one. The last keyframe’s easing is therefore unused.

### *class* an.stage.timeline.LoopMode(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

How a clip behaves past its natural duration.

### *class* an.stage.timeline.PlacedClip(clip, start_time=0.0, duration=None, speed=1.0, blend_in=0.0, blend_out=0.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A clip placed at an absolute time on a track.

#### *property* effective_duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Duration this clip occupies on the timeline (after speed scaling).

### *class* an.stage.timeline.Timeline(duration, tracks=<factory>, space=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A duration + ordered list of tracks. The canonical playback structure.

`space` is the property space the timeline’s DOCUMENT declares for its
targets (an#245): one space, a registered name, or a `target -> space`
resolver. It is what [`evaluate_timeline()`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.evaluate_timeline) uses when the caller passes
none, so every caller of the default evaluates a target in the space its
entity’s kind declares. `None` (every document that declares nothing) is
the kernel default, [`an.timing.spaces.DFLT_TIMELINE_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_TIMELINE_SPACE).

### *class* an.stage.timeline.Track(target_root='', clips=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A sequence of placed clips that share a common purpose / target prefix.

`target_root` is informational metadata for downstream tools (the JS
runtime can use it to scope rendering); evaluation does not filter by it.

### *class* an.stage.timeline.Transform2D(x=0.0, y=0.0, rotation=0.0, scale_x=1.0, scale_y=1.0, pivot_x=0.0, pivot_y=0.0, rotation_x=0.0, eye_distance=0.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One node’s local transform, in the runtime’s own vocabulary.

Field names and defaults mirror `applyTransform` in `runtime.js` exactly —
`x`, `y`, `rotation`, `scale_x`, `scale_y`, `pivot_x`, `pivot_y` — because
the point of this class is to agree with the vendored engine rather than to
re-derive it. `skew` is deliberately absent: PixiJS composes skew into the
same matrix, but no emitter in this package produces a skew channel, and a
field nothing writes is a claim this compositor cannot honour.

#### apply(point)

This node’s local point, in its PARENT’s coordinates.

`world = position + M·(local − pivot)` — the composition PixiJS
performs, and the reason `root.pivot` is a 2D camera: moving the pivot
moves everything the node contains, in the opposite direction.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> Transform2D(x=10.0).apply((0.0, 0.0))
(10.0, 0.0)
>>> Transform2D(pivot_x=25.0).apply((0.0, 0.0))
(-25.0, 0.0)
>>> Transform2D(scale_x=2.0).apply((5.0, 0.0))
(10.0, 0.0)
```

#### project(q)

A plane point `q = local − pivot` as the tilted plane shows it.

`k = f / (f − q_y·sin θ)`, `P(q) = (k·q_x, k·q_y·cos θ)`: the hinge
row (`q_y = 0`) is unmoved, and a row up a receding plane is pulled
toward the horizon at `f·cot θ` above it.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> t = Transform2D(rotation_x=math.pi / 3, eye_distance=1000.0)
>>> t.project((100.0, 0.0))
(100.0, 0.0)
>>> x, y = t.project((100.0, -500.0))
>>> x < 100.0 and -500.0 < y < 0.0
True
```

A point behind the eye (or a plane edge-on, or no eye distance) is one
the runtime does not draw, so it has no position:

```pycon
>>> t.project((0.0, 5000.0))
Traceback (most recent call last):
...
ValueError: plane point (0.0, 5000.0) is not drawn: ...
```

#### rotation_x *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The plane’s tilt (an#314, radians; positive = the top recedes) and the
eye’s distance from the hinge in scene px (`perspective` × the frame
height). The projection is applied to `local − pivot`, before the 2D
transform, exactly as `runtime.js` “Planes” draws it.

#### unapply(point)

The inverse of [`apply()`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Transform2D.apply) — a parent-space point, in local space.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> t = Transform2D(x=10.0, pivot_x=3.0, scale_x=2.0, rotation=0.4)
>>> round(t.unapply(t.apply((7.0, -2.0)))[0], 9)
7.0
```

#### unproject(p)

The inverse of [`project()`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Transform2D.project).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> t = Transform2D(rotation_x=1.0, eye_distance=800.0)
>>> [round(v, 9) for v in t.unproject(t.project((40.0, -300.0)))]
[40.0, -300.0]
```

### an.stage.timeline.clip_from_json(anim, , name=None)

One compiled animation (`compiled.schema.json`’s `animation`) as a [`Clip`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Clip).

Accepts the JSON mapping or any object with the same attributes (the stage’s
`AnimationClipJSON`). Two fields are carried rather than defaulted, and
both have cost a bug: `loop_mode` (without it every loop evaluated as
`once` — an#7) and a list-valued `easing`, which is a cubic-bezier
control quadruple and must stay a tuple for `Keyframe`. The stage compiler
reads a from-less tween’s start through this too (an#212), so it evaluates
exactly what the runtime will.

* **Return type:**
  [`Clip`](_autosummary/an.timing.clip.html.md#an.timing.clip.Clip)

### an.stage.timeline.evaluate_timeline(timeline, t, , space=None)

Evaluate `timeline` at time `t`, merging poses across tracks/clips.

The result is a PURE function of `t` (an#185): what a node shows at `t`
never depends on which instants were evaluated before it. Per
`(target, property)`:

- **Active** — some clip writing it is playing at `t` (inclusive end:
  a clip at `[s, e]` is active at `t == e` too, so the final frame of
  “play this from 0 to 1 s” is visible at 1.0). Later wins: track order,
  then clip order within a track. Written at `t`.
- **Held** — no clip writing it is playing, but one has ended: the value
  the clip reached AT ITS END holds. The latest end wins; a tie goes to
  the later clip, the same “later wins” as above. Written at that end.
- **At rest** — nothing writing it has started yet. The key is ABSENT from
  the pose, and its value is the node’s own (the entity’s rest state;
  `runtime.js` restores what it built).

Keys that write the same thing on one node (a write group: the swap sets of
one visual, `rotation`/`rotation_rad`) keep only the most recently
WRITTEN — an ended `viseme@happy` span does not outlive the `viseme`
track that took the mouth back.

`space` says what each property is ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)): one space, a
registered space’s name, or a `target -> space` resolver. Its field kinds
interpolate and its write groups resolve. `None` is the default space,
[`an.timing.spaces.DFLT_TIMELINE_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_TIMELINE_SPACE) (the stage node’s declared
kinds, by name at call time). `space=VALUE_TYPED` is the rule
`runtime.js` implements — interpolation by value type, the `stage.node`
write groups — which gives the same pose on every timeline the compiler
emits (it refuses a value that fails its field kind).

Forward-order rendering used to show the value at the clip’s last SAMPLED
frame instead (the runtime kept whatever it last applied). The two agree
whenever a clip ends on the frame grid — true of every golden-corpus clip
— and differ when it ends between frames: a 0.37 s tween to 10 at 24 fps
used to stop at 9.80 and now lands on 10, as authored. That landing is
deliberate (it is the bug the motion presets’ settling `set` patched one
preset at a time), and it is what makes the pose independent of the grid.
Also deliberate: a clip shorter than a frame that no frame lands in now
leaves its end value, and a held descendant tint stays on top of an
ancestor’s later tint (the more specific target wins, as it always did
while both played).

`runtime.js::evaluateTimeline` is a port of this function and
`cutan`’s `tests/test_pure_pose.py` holds the two to it.

```pycon
>>> from an.timing.channel import Channel, Keyframe
>>> from an.timing.clip import Clip
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> tl = Timeline(2.0, [Track("a", [PlacedClip(Clip("m", 1.0, [ch]), 0.5)])])
>>> evaluate_timeline(tl, 0.0)  # not started: at rest, so absent
{}
>>> evaluate_timeline(tl, 1.0)[("a", "x")]  # active
5.0
>>> evaluate_timeline(tl, 1.75)[("a", "x")]  # ended: its end value holds
10.0
```

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.stage.timeline.merge_poses(\*poses)

Merge multiple poses with **override semantics** (later wins per key).

Used by the timeline to combine concurrent clips on the same target.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> merge_poses({("a", "x"): 1.0}, {("a", "x"): 2.0, ("a", "y"): 3.0})
{('a', 'x'): 2.0, ('a', 'y'): 3.0}
```

### an.stage.timeline.node_box(scene, path, , pose=None)

The canvas rectangle `(x0, y0, x1, y1)` the visual at `path` occupies (an#445).

The visual’s box, the one the runtime fits its art into (`width` ×
`height` about its anchor, plus a swap key’s own box, anchor and offset
when the key in force has one, an#211), with its four corners composed up
the chain by [`screen_position()`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.screen_position). So it rotates, scales and pans with
the node, its parents and the camera, exactly as a point does. `None`
when the node draws nothing.

Under `fit: contain` the art keeps its aspect inside the box, so the
rectangle is an upper bound on what is painted: exact on the fitted axis,
with the slack the art’s aspect leaves on the other. A stroked path’s box
is its polyline’s, grown by half the stroke.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> from an.stage.serialize import CutoutSceneJSON, NodeJSON, TimelineJSON, TransformJSON, VisualJSON
>>> scene = CutoutSceneJSON(
...     scene=NodeJSON(name="root", children=[NodeJSON(
...         name="card", transform=TransformJSON(x=40.0),
...         visual=VisualJSON(kind="rect", width=20.0, height=10.0))]),
...     timeline=TimelineJSON(duration=1.0),
... )
>>> scene.meta.width, scene.meta.height = 320, 240
>>> node_box(scene, "card")
(190.0, 115.0, 210.0, 125.0)
>>> node_box(scene, "card", pose={("card", "scale_x"): 2.0})
(180.0, 115.0, 220.0, 125.0)
```

### an.stage.timeline.screen_position(scene, path, , pose=None, point=(0.0, 0.0))

Where `point` in `path`’s local space lands on the canvas.

The composition the runtime performs, walked from the node up to `root`
and then offset by the canvas centre — which is where `runtime.js` places
the root container.

`pose` is keyed by the FULL path (`"street/hills"`), matching what
`evaluate_timeline` returns, and each node reads only its own entry.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> from an.stage.serialize import CutoutSceneJSON, NodeJSON, TimelineJSON, TransformJSON
>>> scene = CutoutSceneJSON(
...     scene=NodeJSON(name="root", children=[
...         NodeJSON(name="hill", transform=TransformJSON(x=40.0))]),
...     timeline=TimelineJSON(duration=1.0),
... )
>>> scene.meta.width, scene.meta.height = 320, 240
>>> screen_position(scene, "hill")
(200.0, 120.0)
```

…and moving the camera’s pivot moves it the other way, which is the whole
reason `root.pivot` is the camera:

```pycon
>>> screen_position(scene, "hill", pose={("root", "pivot_x"): 25.0})
(175.0, 120.0)
```

### an.stage.timeline.timeline_from_scene(scene)

The compiled scene’s `timeline`/`animations` as an evaluable `Timeline`.

`compile_shot` produces a serialisable document (`an.stage.serialize`)
for the JS runtime; this rebuilds the *evaluable* form, so a caller can ask
what a compiled scene’s pose is at time `t` without a browser. It is the
Python side of the parity contract: `evaluate_timeline` over this object is
the executable spec `runtime.js` is tested against. The reading itself is the
kernel’s ([`an.timing.timeline.timeline_from_compiled()`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.timeline_from_compiled)), which carries
`loop_mode` and keeps a list-valued `easing` a tuple.

* **Return type:**
  [`Timeline`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.Timeline)

```pycon
>>> from an.stage.compile import compile_shot
>>> from an.ir.compose import tween
>>> from an.ir.schema import Shot
>>> shot = Shot(id="s1", renderer="cutout", duration=2.0,
...             actions=[tween("root", "x", 10.0, 1.0, from_=0.0)])
>>> scene = compile_shot(shot, mall=None, fps=24)
>>> evaluate_timeline(timeline_from_scene(scene), 0.5)[("root", "x")]
5.0
```

### an.stage.timeline.transform_of(node, pose=None, , frame_height=1080.0)

A node’s transform, with `pose` overriding what the document declares.

The runtime applies a pose value by assigning the property on the display
object, so a channel REPLACES the declared value rather than adding to it —
which is why the parallax compensation carries the plane’s own offset in
every keyframe instead of an offset from it. `frame_height` turns a
`perspective` (in frame heights, an#314) into the eye’s distance.

* **Return type:**
  [`Transform2D`](_autosummary/an.stage.timeline.html.md#an.stage.timeline.Transform2D)

### an.stage.timeline.write_group(prop)

What `prop` writes on a STAGE node (the `stage.node` space’s groups).

Two keys in one group set the same thing, so only the more recently written
can be showing: every swap set on a node swaps the one visual it carries
(`viseme` and `viseme@happy` both set the mouth’s texture, an#88), and
`rotation_rad` is `rotation`. Every other runtime property
(`an.base.TRANSFORM_PROPERTIES`, the runtime’s own switch) writes only
itself.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> write_group("x"), write_group("rotation_rad"), write_group("viseme@happy")
('x', 'rotation', '<swap>')
```


# _autosummary/an.stage.tree.html.md

# an.stage.tree

The scene tree’s paths: the ONE Python statement of how `runtime.js` indexes nodes (an#343).

The runtime builds a container per node and indexes it in `nodeIndex` under a
slash-joined path; every channel target is such a path. The compiled scene’s
top-level `root` is synthetic and is not indexed (its children start at the
entity name), and the overlay’s children are indexed the same way. Since an#343
a node may carry `scope`: its CHILDREN are indexed as if they were the
children of the node named `scope` in the same parent, so an environment’s
foreground container (`set__front`, `scope="set"`) indexes its planes as
`set/<plane>`, wherever the environment was cut. `scope=""` indexes them
as the parent’s own children (a wrapper that adds a transform and no path
segment).

```pycon
>>> from an.stage.serialize import NodeJSON
>>> root = NodeJSON(name="root", children=[
...     NodeJSON(name="set", children=[NodeJSON(name="sky")]),
...     NodeJSON(name="maya"),
...     NodeJSON(name="set__front", scope="set", children=[NodeJSON(name="wall")]),
... ])
>>> sorted(paths(root))
['maya', 'set', 'set/sky', 'set/wall', 'set__front']
>>> [p for _, p in chain(root, "set/wall")]
['root', 'set__front', 'set/wall']
```

The runtime’s own copy of the rule is `childPrefix` in `runtime.js`;
`tests/test_stage_tree.py` runs it under node against [`child_prefix()`](_autosummary/an.stage.tree.html.md#an.stage.tree.child_prefix),
and a lint test there refuses a new hand-rolled walk that joins names.

Works on the wire models ([`an.stage.serialize`](_autosummary/an.stage.serialize.html.md#module-an.stage.serialize)) and on their JSON dicts
alike, since the bench reads the staged document as JSON.

### Module Attributes

| [`SYNTHETIC_ROOT`](_autosummary/an.stage.tree.html.md#an.stage.tree.SYNTHETIC_ROOT)   | never indexed by the runtime.   |
|-------------------------------------------------------------------|---------------------------------|

### Functions

| [`child_prefix`](_autosummary/an.stage.tree.html.md#an.stage.tree.child_prefix)(node, path, prefix)   | The prefix `node`'s children are indexed under: its own `path`, or, with `scope`, `scope` taken in the parent's prefix (`""`: the parent's prefix itself).                                                  |
|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`join`](_autosummary/an.stage.tree.html.md#an.stage.tree.join)(prefix, name)                 | `prefix/name`, or `name` at the top.                                                                                                                                                                        |
| [`walk`](_autosummary/an.stage.tree.html.md#an.stage.tree.walk)(node[, prefix])               | `(path, node)` for `node` and every descendant, as the runtime indexes them, in document order.                                                                                                             |
| [`lineage`](_autosummary/an.stage.tree.html.md#an.stage.tree.lineage)(node[, prefix])            | `(path, node, ancestor paths)` for `node` and every descendant: the paths of the containers it is drawn inside, outermost first.                                                                            |
| [`walk_children`](_autosummary/an.stage.tree.html.md#an.stage.tree.walk_children)(root)                | `walk` over the children of the synthetic `root` (not indexed).                                                                                                                                             |
| [`walk_document`](_autosummary/an.stage.tree.html.md#an.stage.tree.walk_document)(doc)                 | Every indexed `(path, node)` of a compiled document: the scene's entities, then the overlay's (one index for both layers).                                                                                  |
| [`paths`](_autosummary/an.stage.tree.html.md#an.stage.tree.paths)(root)                        | Every path the runtime indexes under the synthetic `root`.                                                                                                                                                  |
| [`node_at`](_autosummary/an.stage.tree.html.md#an.stage.tree.node_at)(root, path)                | The node the runtime indexes at `path` (later wins, as `nodeIndex` does), or raise `KeyError` naming the top-level nodes.                                                                                   |
| [`chain`](_autosummary/an.stage.tree.html.md#an.stage.tree.chain)(root, path)                  | `[(node, its indexed path)]` from the synthetic `root` down to the node at `path`, every container on the way included (a scoped container composes like any parent: it is a container in the runtime too). |

### an.stage.tree.SYNTHETIC_ROOT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'root'*

never indexed by the runtime.

* **Type:**
  The compiled scene’s top-level container

### an.stage.tree.chain(root, path)

`[(node, its indexed path)]` from the synthetic `root` down to the
node at `path`, every container on the way included (a scoped container
composes like any parent: it is a container in the runtime too).

Raises `KeyError` naming what is there rather than measuring the wrong node.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### an.stage.tree.child_prefix(node, path, prefix)

The prefix `node`’s children are indexed under: its own `path`, or,
with `scope`, `scope` taken in the parent’s prefix (`""`: the
parent’s prefix itself). Mirrors `childPrefix` in `runtime.js`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from an.stage.serialize import NodeJSON
>>> child_prefix(NodeJSON(name="a"), "env/a", "env")
'env/a'
>>> child_prefix(NodeJSON(name="a", scope="b"), "env/a", "env")
'env/b'
>>> child_prefix(NodeJSON(name="a", scope=""), "env/a", "env")
'env'
```

### an.stage.tree.join(prefix, name)

`prefix/name`, or `name` at the top.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> join("", "set"), join("set", "sky")
('set', 'set/sky')
```

### an.stage.tree.lineage(node, prefix='')

`(path, node, ancestor paths)` for `node` and every descendant: the
paths of the containers it is drawn inside, outermost first.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]]]

### an.stage.tree.node_at(root, path)

The node the runtime indexes at `path` (later wins, as `nodeIndex`
does), or raise `KeyError` naming the top-level nodes.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.stage.tree.paths(root)

Every path the runtime indexes under the synthetic `root`.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.stage.tree.walk(node, prefix='')

`(path, node)` for `node` and every descendant, as the runtime
indexes them, in document order.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.stage.tree.walk_children(root)

`walk` over the children of the synthetic `root` (not indexed).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### an.stage.tree.walk_document(doc)

Every indexed `(path, node)` of a compiled document: the scene’s
entities, then the overlay’s (one index for both layers).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]


# _autosummary/an.stores.artifacts.html.md

# an.stores.artifacts

Artifact stores — derived, regeneratable products of the pipeline.

All artifact stores are content-addressed: the key is meant to be a hash of
the IR slice that produced the artifact. This lets later phases skip re-render
when the inputs haven’t changed.

In Phase 1 these are simple file-backed stores; the content-hash convention is
enforced by the caller (orchestrator), not the store.

### Classes

| [`AudioArtifactStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.AudioArtifactStore)(root_dir)   | TTS-rendered audio clips (.wav).                                                                                                                                                                                  |
|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`CaptionsStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.CaptionsStore)(root_dir)        | SubRip caption sidecars (.srt, UTF-8 bytes), keyed like the output they caption — `captions["main"]` is `output/main.srt` (an#175).                                                                               |
| [`ContactSheetStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.ContactSheetStore)(root_dir)    | Contact sheets (.png): frames of a render at its settled beats, labelled with their time, for a person or an agent to LOOK at (core study §2.10).                                                                 |
| [`MeasurementStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.MeasurementStore)(root_dir)     | Measurements a clock-owning renderer made of its content (.json bytes), keyed by that content's key — DERIVED data, never the author's (an#279): a Manim shot's length, its timeline of beats, its findings.      |
| [`OutputStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.OutputStore)(root_dir)          | Final composited renders.                                                                                                                                                                                         |
| [`PictureStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.PictureStore)(root_dir)         | An opaque renderer's raw picture (.mp4) — a Manim scene's own render, before it is conformed to a film's fps and size, so a change that is not to the picture (narration, fps) does not run Manim again (an#279). |
| [`PreviewArtifactStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.PreviewArtifactStore)(root_dir) | Low-res preview renders (mp4 or png sequence wrapper).                                                                                                                                                            |
| [`RenderReportStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.RenderReportStore)(root_dir)    | What a render found (.json), keyed like the output it describes — `render_reports["main"]` — so `orchestrate` and MCP read the findings that `an render` warned (an#279).                                         |
| [`ShotArtifactStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.ShotArtifactStore)(root_dir)    | Per-shot rendered mp4s.                                                                                                                                                                                           |
| [`TakesArtifactStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.TakesArtifactStore)(root_dir)   | Best-of-N take records (.json bytes), keyed by the line's audio key: which take was kept, its sha256, every take's score and the scorer (an#265).                                                                 |
| [`VisemeArtifactStore`](_autosummary/an.stores.artifacts.html.md#an.stores.artifacts.VisemeArtifactStore)(root_dir)  | Lip-sync viseme tracks (.json) — stored as bytes for cache uniformity.                                                                                                                                            |

### *class* an.stores.artifacts.AudioArtifactStore(root_dir)

Bases: `_BlobStore`

TTS-rendered audio clips (.wav).

### *class* an.stores.artifacts.CaptionsStore(root_dir)

Bases: `_BlobStore`

SubRip caption sidecars (.srt, UTF-8 bytes), keyed like the output they
caption — `captions["main"]` is `output/main.srt` (an#175).

### *class* an.stores.artifacts.ContactSheetStore(root_dir)

Bases: `_BlobStore`

Contact sheets (.png): frames of a render at its settled beats, labelled
with their time, for a person or an agent to LOOK at (core study §2.10).

Content-addressed — the key is the sha256 of the PNG bytes — so a shot whose
render is reused from the shot cache still points at its sheet through the
cached provenance, and two shots with one picture share one file.

### *class* an.stores.artifacts.MeasurementStore(root_dir)

Bases: `_BlobStore`

Measurements a clock-owning renderer made of its content (.json bytes),
keyed by that content’s key — DERIVED data, never the author’s (an#279):
a Manim shot’s length, its timeline of beats, its findings.

### *class* an.stores.artifacts.OutputStore(root_dir)

Bases: `_BlobStore`

Final composited renders.

### *class* an.stores.artifacts.PictureStore(root_dir)

Bases: `_BlobStore`

An opaque renderer’s raw picture (.mp4) — a Manim scene’s own render,
before it is conformed to a film’s fps and size, so a change that is not to
the picture (narration, fps) does not run Manim again (an#279).
Content-addressed (the key is the sha256 of the mp4): the measurement
record under the picture key names it, so a record and its picture are
written as one (an#291).

### *class* an.stores.artifacts.PreviewArtifactStore(root_dir)

Bases: `_BlobStore`

Low-res preview renders (mp4 or png sequence wrapper).

### *class* an.stores.artifacts.RenderReportStore(root_dir)

Bases: `_BlobStore`

What a render found (.json), keyed like the output it describes —
`render_reports["main"]` — so `orchestrate` and MCP read the findings
that `an render` warned (an#279).

### *class* an.stores.artifacts.ShotArtifactStore(root_dir)

Bases: `_BlobStore`

Per-shot rendered mp4s.

### *class* an.stores.artifacts.TakesArtifactStore(root_dir)

Bases: `_BlobStore`

Best-of-N take records (.json bytes), keyed by the line’s audio key: which
take was kept, its sha256, every take’s score and the scorer (an#265).

### *class* an.stores.artifacts.VisemeArtifactStore(root_dir)

Bases: `_BlobStore`

Lip-sync viseme tracks (.json) — stored as bytes for cache uniformity.


# _autosummary/an.stores.characters.html.md

# an.stores.characters

Characters store — descriptor + sidecar folder per character.

A character’s `character.json` is the Phase 11a CharacterDescriptor
(slot/skin/animation graph + viseme map). Binary parts (SVGs, PNGs) are
sidecars under the same directory at e.g. `parts/head.svg`,
`parts/mouth/mouth_a.svg`, etc.

### Classes

| [`CharactersStore`](_autosummary/an.stores.characters.html.md#an.stores.characters.CharactersStore)(root_dir)   | Per-character directory store.   |
|------------------------------------------------------------------------------|----------------------------------|

### *class* an.stores.characters.CharactersStore(root_dir)

Bases: `JsonSidecarStore`

Per-character directory store.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = CharactersStore(d)
...     store['maya'] = {'name': 'Maya', 'voice_ref': 'maya-warm'}
...     store['maya']['name']
'Maya'
```


# _autosummary/an.stores.decisions.html.md

# an.stores.decisions

Decision log — append-only JSONL of agent decisions and user approvals.

Each call to `append` writes one line to `.an/decisions.jsonl`. Reading
yields decisions in order. The store exposes a `MutableMapping` interface keyed
by integer index (as a string) for uniformity with the other stores, plus an
`append` convenience method.

### Classes

| [`DecisionLogStore`](_autosummary/an.stores.decisions.html.md#an.stores.decisions.DecisionLogStore)(log_path)   | Append-only JSONL log keyed by ordinal index (as string).   |
|-------------------------------------------------------------------------------|-------------------------------------------------------------|

### *class* an.stores.decisions.DecisionLogStore(log_path)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

Append-only JSONL log keyed by ordinal index (as string).

Mutating in-place (`__setitem__`, `__delitem__`) is intentionally
forbidden; the log is append-only by design.

```pycon
>>> import tempfile, os
>>> with tempfile.TemporaryDirectory() as d:
...     log = DecisionLogStore(os.path.join(d, 'decisions.jsonl'))
...     _ = log.append(kind='test', body={'x': 1})
...     _ = log.append(kind='test', body={'x': 2})
...     entries = list(log.values())
...     entries[0]['body']['x'], entries[1]['body']['x']
(1, 2)
```

#### append(, kind, body, \*\*extra)

Append one decision; returns its ordinal index.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)


# _autosummary/an.stores.environments.html.md

# an.stores.environments

Environments store — backgrounds, set pieces, and prop bundles.

### Classes

| [`EnvironmentsStore`](_autosummary/an.stores.environments.html.md#an.stores.environments.EnvironmentsStore)(root_dir)   | Per-environment directory store (meta + sidecar art).   |
|--------------------------------------------------------------------------------|---------------------------------------------------------|

### *class* an.stores.environments.EnvironmentsStore(root_dir)

Bases: `JsonSidecarStore`

Per-environment directory store (meta + sidecar art).


# _autosummary/an.stores.html.md

# an.stores

Project mall: a dict of dol-backed `MutableMapping` stores.

The mall is the unit of persistence in an. Every long-lived state — assets
(characters, props, environments, voices, styles, sounds), the scene file pair, intermediate
artifacts (audio, viseme tracks, best-of-N take records, per-shot mp4s, the
content-keyed shot cache),
final output and its caption sidecar, the agent’s decision log, and the asset
library’s lockfile of pinned check-outs — is keyed inside a store. Stores are
dol-backed so the same call
sites work against filesystem, SQLite, S3, etc.

```pycon
>>> import tempfile
>>> from pathlib import Path
>>> with tempfile.TemporaryDirectory() as d:
...     mall = build_project_mall(d, ensure=True)
...     sorted(mall.keys()) == [
...         'audio', 'captions', 'characters', 'contact_sheets', 'decisions',
...         'environments', 'library_lock', 'measurements', 'output',
...         'pictures', 'previews', 'props', 'render_reports', 'scenes',
...         'shot_cache', 'shots', 'sounds', 'sources', 'styles', 'takes',
...         'visemes', 'voices',
...     ]
True
```

### Functions

| [`build_project_mall`](_autosummary/an.stores.html.md#an.stores.build_project_mall)(project_dir, \*[, ensure])   | Build the standard project mall over `project_dir`.   |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------|

### Classes

| [`CharactersStore`](_autosummary/an.stores.html.md#an.stores.CharactersStore)(root_dir)      | Per-character directory store.                                                                                                                                                                                    |
|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`EnvironmentsStore`](_autosummary/an.stores.html.md#an.stores.EnvironmentsStore)(root_dir)    | Per-environment directory store (meta + sidecar art).                                                                                                                                                             |
| [`VoicesStore`](_autosummary/an.stores.html.md#an.stores.VoicesStore)(root_dir)          | JSON-only voice descriptors.                                                                                                                                                                                      |
| [`StylesStore`](_autosummary/an.stores.html.md#an.stores.StylesStore)(root_dir)          | Pure-JSON style descriptors.                                                                                                                                                                                      |
| [`PropsStore`](_autosummary/an.stores.html.md#an.stores.PropsStore)(root_dir)           | Per-prop directory store.                                                                                                                                                                                         |
| [`ScenesStore`](_autosummary/an.stores.html.md#an.stores.ScenesStore)(project_dir)       | `MutableMapping` exposing the scene file pair under a project root.                                                                                                                                               |
| [`SoundsStore`](_autosummary/an.stores.html.md#an.stores.SoundsStore)(root_dir)          | Per-sound directory store.                                                                                                                                                                                        |
| [`AudioArtifactStore`](_autosummary/an.stores.html.md#an.stores.AudioArtifactStore)(root_dir)   | TTS-rendered audio clips (.wav).                                                                                                                                                                                  |
| [`VisemeArtifactStore`](_autosummary/an.stores.html.md#an.stores.VisemeArtifactStore)(root_dir)  | Lip-sync viseme tracks (.json) — stored as bytes for cache uniformity.                                                                                                                                            |
| [`TakesArtifactStore`](_autosummary/an.stores.html.md#an.stores.TakesArtifactStore)(root_dir)   | Best-of-N take records (.json bytes), keyed by the line's audio key: which take was kept, its sha256, every take's score and the scorer (an#265).                                                                 |
| [`ShotArtifactStore`](_autosummary/an.stores.html.md#an.stores.ShotArtifactStore)(root_dir)    | Per-shot rendered mp4s.                                                                                                                                                                                           |
| [`PreviewArtifactStore`](_autosummary/an.stores.html.md#an.stores.PreviewArtifactStore)(root_dir) | Low-res preview renders (mp4 or png sequence wrapper).                                                                                                                                                            |
| [`ContactSheetStore`](_autosummary/an.stores.html.md#an.stores.ContactSheetStore)(root_dir)    | Contact sheets (.png): frames of a render at its settled beats, labelled with their time, for a person or an agent to LOOK at (core study §2.10).                                                                 |
| [`MeasurementStore`](_autosummary/an.stores.html.md#an.stores.MeasurementStore)(root_dir)     | Measurements a clock-owning renderer made of its content (.json bytes), keyed by that content's key — DERIVED data, never the author's (an#279): a Manim shot's length, its timeline of beats, its findings.      |
| [`PictureStore`](_autosummary/an.stores.html.md#an.stores.PictureStore)(root_dir)         | An opaque renderer's raw picture (.mp4) — a Manim scene's own render, before it is conformed to a film's fps and size, so a change that is not to the picture (narration, fps) does not run Manim again (an#279). |
| [`RenderReportStore`](_autosummary/an.stores.html.md#an.stores.RenderReportStore)(root_dir)    | What a render found (.json), keyed like the output it describes — `render_reports["main"]` — so `orchestrate` and MCP read the findings that `an render` warned (an#279).                                         |
| [`SourcesStore`](_autosummary/an.stores.html.md#an.stores.SourcesStore)(root_dir)         | Python scene sources (`.py` bytes) — a Manim scene file per key.                                                                                                                                                  |
| [`OutputStore`](_autosummary/an.stores.html.md#an.stores.OutputStore)(root_dir)          | Final composited renders.                                                                                                                                                                                         |
| [`DecisionLogStore`](_autosummary/an.stores.html.md#an.stores.DecisionLogStore)(log_path)     | Append-only JSONL log keyed by ordinal index (as string).                                                                                                                                                         |
| [`ProjectLock`](_autosummary/an.stores.html.md#an.stores.ProjectLock)(project_dir)       | `<store>/<key> -> pin` over a project's `assets.lock.json`.                                                                                                                                                       |

### *class* an.stores.AudioArtifactStore(root_dir)

Bases: `_BlobStore`

TTS-rendered audio clips (.wav).

### *class* an.stores.CharactersStore(root_dir)

Bases: `JsonSidecarStore`

Per-character directory store.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = CharactersStore(d)
...     store['maya'] = {'name': 'Maya', 'voice_ref': 'maya-warm'}
...     store['maya']['name']
'Maya'
```

### *class* an.stores.ContactSheetStore(root_dir)

Bases: `_BlobStore`

Contact sheets (.png): frames of a render at its settled beats, labelled
with their time, for a person or an agent to LOOK at (core study §2.10).

Content-addressed — the key is the sha256 of the PNG bytes — so a shot whose
render is reused from the shot cache still points at its sheet through the
cached provenance, and two shots with one picture share one file.

### *class* an.stores.DecisionLogStore(log_path)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

Append-only JSONL log keyed by ordinal index (as string).

Mutating in-place (`__setitem__`, `__delitem__`) is intentionally
forbidden; the log is append-only by design.

```pycon
>>> import tempfile, os
>>> with tempfile.TemporaryDirectory() as d:
...     log = DecisionLogStore(os.path.join(d, 'decisions.jsonl'))
...     _ = log.append(kind='test', body={'x': 1})
...     _ = log.append(kind='test', body={'x': 2})
...     entries = list(log.values())
...     entries[0]['body']['x'], entries[1]['body']['x']
(1, 2)
```

#### append(, kind, body, \*\*extra)

Append one decision; returns its ordinal index.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### *class* an.stores.EnvironmentsStore(root_dir)

Bases: `JsonSidecarStore`

Per-environment directory store (meta + sidecar art).

### *class* an.stores.MeasurementStore(root_dir)

Bases: `_BlobStore`

Measurements a clock-owning renderer made of its content (.json bytes),
keyed by that content’s key — DERIVED data, never the author’s (an#279):
a Manim shot’s length, its timeline of beats, its findings.

### *class* an.stores.OutputStore(root_dir)

Bases: `_BlobStore`

Final composited renders.

### *class* an.stores.PictureStore(root_dir)

Bases: `_BlobStore`

An opaque renderer’s raw picture (.mp4) — a Manim scene’s own render,
before it is conformed to a film’s fps and size, so a change that is not to
the picture (narration, fps) does not run Manim again (an#279).
Content-addressed (the key is the sha256 of the mp4): the measurement
record under the picture key names it, so a record and its picture are
written as one (an#291).

### *class* an.stores.PreviewArtifactStore(root_dir)

Bases: `_BlobStore`

Low-res preview renders (mp4 or png sequence wrapper).

### *class* an.stores.ProjectLock(project_dir)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

`<store>/<key> -> pin` over a project’s `assets.lock.json`.

Every write rewrites the whole (small) file, sorted, so the lockfile diffs
cleanly under version control. The mapping is the `assets` section;
[`kits`](_autosummary/an.stores.html.md#an.stores.ProjectLock.kits) is the `kits` section, and a write to either keeps the other.

#### *property* kits *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]*

`kit asset id -> record` of the kits checked out into the project.

### *class* an.stores.PropsStore(root_dir)

Bases: `JsonSidecarStore`

Per-prop directory store.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = PropsStore(d)
...     store['lamp'] = {'name': 'Desk lamp'}
...     store['lamp']['name']
'Desk lamp'
```

### *class* an.stores.RenderReportStore(root_dir)

Bases: `_BlobStore`

What a render found (.json), keyed like the output it describes —
`render_reports["main"]` — so `orchestrate` and MCP read the findings
that `an render` warned (an#279).

### *class* an.stores.ScenesStore(project_dir)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

`MutableMapping` exposing the scene file pair under a project root.

Keys: currently always `"main"`. The store enforces this by raising
`KeyError` for other keys.

#### patch_shot_durations(durations)

Set `duration` on the named shots WITHOUT regenerating `scene.md`.

`__setitem__` rewrites the markdown from the IR, which drops every word
of prose and every comment the IR does not hold. This writes the JSON
through the read boundary as usual, and patches only each shot’s
`duration:` line in the markdown (inserting one into its

```
``
```

\`\` ``yaml shot ``` block, or the block itself, when there is none) —
what `an sync --accept-measured` uses to take a measured duration into
the authored scene (an#279).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* an.stores.ShotArtifactStore(root_dir)

Bases: `_BlobStore`

Per-shot rendered mp4s.

### *class* an.stores.SoundsStore(root_dir)

Bases: `JsonSidecarStore`

Per-sound directory store.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = SoundsStore(d)
...     store['hit'] = {'description': 'a stick on a table'}
...     store.write_audio('hit', b'RIFF....')
...     store['hit']['description'], store.read_audio('hit')
('a stick on a table', b'RIFF....')
```

#### AUDIO_NAME *= 'audio.wav'*

`an.sounds` reads
its header for the duration a fade-out needs, deterministically.

* **Type:**
  The sidecar holding the audio bytes. WAV only in v1

### *class* an.stores.SourcesStore(root_dir)

Bases: `_BlobStore`

Python scene sources (`.py` bytes) — a Manim scene file per key.

### *class* an.stores.StylesStore(root_dir)

Bases: `JsonDirStore`

Pure-JSON style descriptors.

### *class* an.stores.TakesArtifactStore(root_dir)

Bases: `_BlobStore`

Best-of-N take records (.json bytes), keyed by the line’s audio key: which
take was kept, its sha256, every take’s score and the scorer (an#265).

### *class* an.stores.VisemeArtifactStore(root_dir)

Bases: `_BlobStore`

Lip-sync viseme tracks (.json) — stored as bytes for cache uniformity.

### *class* an.stores.VoicesStore(root_dir)

Bases: `JsonDirStore`

JSON-only voice descriptors.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = VoicesStore(d)
...     store['maya-warm'] = {'provider': 'elevenlabs', 'voice_id': 'xyz'}
...     'maya-warm' in store
True
```

### an.stores.build_project_mall(project_dir, , ensure=False, \*\*overrides)

Build the standard project mall over `project_dir`.

Pass `ensure=True` to create the per-store directories on disk if they
don’t exist. Pass keyword overrides to swap in alternate stores (e.g. an
in-memory `dict` for tests).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`MutableMapping`](https://docs.python.org/3/library/typing.html#typing.MutableMapping)]

### Modules

| [`artifacts`](_autosummary/an.stores.artifacts.html.md#module-an.stores.artifacts)       | Artifact stores — derived, regeneratable products of the pipeline.                |
|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`characters`](_autosummary/an.stores.characters.html.md#module-an.stores.characters)     | Characters store — descriptor + sidecar folder per character.                     |
| [`decisions`](_autosummary/an.stores.decisions.html.md#module-an.stores.decisions)       | Decision log — append-only JSONL of agent decisions and user approvals.           |
| [`environments`](_autosummary/an.stores.environments.html.md#module-an.stores.environments) | Environments store — backgrounds, set pieces, and prop bundles.                   |
| [`library_lock`](_autosummary/an.stores.library_lock.html.md#module-an.stores.library_lock) | The project lockfile: which library version each checked-out asset came from.     |
| [`props`](_autosummary/an.stores.props.html.md#module-an.stores.props)               | Props store — descriptor + sidecar folder per prop.                               |
| [`scenes`](_autosummary/an.stores.scenes.html.md#module-an.stores.scenes)             | Scenes store — wraps the project's `scene.md` + `ir/scene.json` pair.             |
| [`sounds`](_autosummary/an.stores.sounds.html.md#module-an.stores.sounds)             | Sounds store — one directory per sound: `sound.json` beside `audio.wav`.          |
| [`sources`](_autosummary/an.stores.sources.html.md#module-an.stores.sources)           | The project's opaque scene sources: files a whole-shot renderer runs as they are. |
| [`styles`](_autosummary/an.stores.styles.html.md#module-an.stores.styles)             | Styles store — visual style presets (color palette, line weight, fonts).          |
| [`voices`](_autosummary/an.stores.voices.html.md#module-an.stores.voices)             | Voices store — pure JSON; one entry per voice.                                    |


# _autosummary/an.stores.library_lock.html.md

# an.stores.library_lock

The project lockfile: which library version each checked-out asset came from.

`assets.lock.json` at the project root (design §7.2), one entry per checked-out
asset, keyed `<store>/<key>` (`characters/alice`) — the address the scene’s
`AssetRef` already uses:

```json
{"schema_version": "0.1.0",
 "assets": {"characters/alice": {"library": "cutan:character.alice-reiniger@v002",
                                 "manifest_sha256": "…", "checked_out": "…"}}}
```

**The lockfile is the single source of truth for a pin** (an#240). It is written
by the check-out that put the files there, beside the manifest it verified, so
it says what the project actually holds. A scene’s `AssetRef.library` is the
declared intent, an optional restatement of it for the reader; a later
“reference” resolution resolves through the lockfile entry `<store>/<ref>`
too, and treats `library:` as the intent it must match — never as a second
pin. `an validate` (and `an render`, fatally under `--strict-assets`)
reports every scene `library:` that disagrees with the lockfile, or names an
entry the lockfile does not pin (`an.library.checkout.check_pins()`). The
lockfile records no library root: it is committed, and a path would leak.

The pin records **provenance**: where each checked-out copy came from, so a
project can be rebuilt from the library. It is not a content key — the copy in
the project can be edited after it is pinned; `an.library.checkout.verify_checkout()`
says whether it still is its version (`an validate` reports the drift as
`info`: an edited check-out is a fork, not a mistake), and only then may
anything treat the pin as standing for the content.

A **kit** check-out ([`an.library.kits.checkout_kit()`](_autosummary/an.library.kits.html.md#an.library.kits.checkout_kit)) pins each member the
way any check-out does, and additionally records the kit in a top-level
`"kits"` section, keyed by the kit’s asset id:

```default
{"kits": {"kit.reiniger-base": {"library": "cutan:kit.reiniger-base@v002",
                                "manifest_sha256": "…", "checked_out": "…",
                                "members": ["styles/reiniger", "voices/narrator"]}}}
```

`members` are lockfile keys of `assets`. The section sits OUTSIDE `assets`
on purpose: everything that walks the pins (`verify_checkout`, `an validate`,
`check_pins`) iterates the mapping, which is `assets` only, so a kit entry
cannot be mistaken for an asset or collide with a `<store>/<key>` key. It is
additive (an older `an` reads the file and ignores the section, though its
next write drops it) and read through
[`ProjectLock.kits`](_autosummary/an.stores.library_lock.html.md#an.stores.library_lock.ProjectLock.kits).

The lockfile is a `MutableMapping` like every other store (pillar 7),
registered in the project mall as `mall["library_lock"]`
([`an.stores.build_project_mall()`](_autosummary/an.stores.html.md#an.stores.build_project_mall)); `an.library.checkout.checkout()`
writes through the mall’s store unless one is injected (`lock=`). It lives
here, with the other project stores, so building a project mall never imports
the asset library; [`an.library.lock`](_autosummary/an.library.lock.html.md#module-an.library.lock) re-exports it.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     lock = ProjectLock(d)
...     lock["characters/alice"] = {"library": "character.alice@v001"}
...     list(ProjectLock(d).items())
[('characters/alice', {'library': 'character.alice@v001'})]
```

### Module Attributes

| [`LOCKFILE_NAME`](_autosummary/an.stores.library_lock.html.md#an.stores.library_lock.LOCKFILE_NAME)           | The lockfile's name at the project root.                        |
|--------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`LOCKFILE_SCHEMA_VERSION`](_autosummary/an.stores.library_lock.html.md#an.stores.library_lock.LOCKFILE_SCHEMA_VERSION) | The lockfile document's schema version (its own document kind). |

### Functions

| [`lock_key`](_autosummary/an.stores.library_lock.html.md#an.stores.library_lock.lock_key)(store, key)   | The lockfile key of one project store entry.   |
|-------------------------------------------------------------------------|------------------------------------------------|

### Classes

| [`ProjectLock`](_autosummary/an.stores.library_lock.html.md#an.stores.library_lock.ProjectLock)(project_dir)   | `<store>/<key> -> pin` over a project's `assets.lock.json`.   |
|-----------------------------------------------------------------------------|---------------------------------------------------------------|

### an.stores.library_lock.LOCKFILE_NAME *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'assets.lock.json'*

The lockfile’s name at the project root.

### an.stores.library_lock.LOCKFILE_SCHEMA_VERSION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '0.1.0'*

The lockfile document’s schema version (its own document kind).

### *class* an.stores.library_lock.ProjectLock(project_dir)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

`<store>/<key> -> pin` over a project’s `assets.lock.json`.

Every write rewrites the whole (small) file, sorted, so the lockfile diffs
cleanly under version control. The mapping is the `assets` section;
[`kits`](_autosummary/an.stores.library_lock.html.md#an.stores.library_lock.ProjectLock.kits) is the `kits` section, and a write to either keeps the other.

#### *property* kits *: [MutableMapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]*

`kit asset id -> record` of the kits checked out into the project.

### an.stores.library_lock.lock_key(store, key)

The lockfile key of one project store entry.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> lock_key("characters", "alice")
'characters/alice'
```


# _autosummary/an.stores.props.html.md

# an.stores.props

Props store — descriptor + sidecar folder per prop.

A prop is a rig whose art is not a person: a lamp, a sword, a sign. It is
stored exactly like a character — a JSON descriptor beside a `parts/`
folder of SVGs — because the cutout compiler builds both from the same rig
builder, and the store is the ONLY thing that differs about where their art
lives (an#108). What differs is the *descriptor*: a prop declares no face,
no blink, and no humanoid skeleton.

Kept a separate store rather than a `kind` field on the characters store,
for the reason the compiler keeps them apart: `an character validate`
scores a correct prop as 21 blocking findings, and the character
placeholder fallback draws a *person* where a lamp should be.

### Classes

| [`PropsStore`](_autosummary/an.stores.props.html.md#an.stores.props.PropsStore)(root_dir)   | Per-prop directory store.   |
|-------------------------------------------------------------------------|-----------------------------|

### *class* an.stores.props.PropsStore(root_dir)

Bases: `JsonSidecarStore`

Per-prop directory store.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = PropsStore(d)
...     store['lamp'] = {'name': 'Desk lamp'}
...     store['lamp']['name']
'Desk lamp'
```


# _autosummary/an.stores.scenes.html.md

# an.stores.scenes

Scenes store — wraps the project’s `scene.md` + `ir/scene.json` pair.

Right now an an project contains exactly one scene (“main”), so this store
exposes only the `"main"` key. Future versions can support multi-scene
projects by promoting siblings inside a `scenes/` directory.

Reading returns a `SceneIR`. Writing accepts a `SceneIR` (or a dict that
validates as one) and persists the JSON and the Markdown – the Markdown UPDATED
rather than regenerated (an#275): unchanged content leaves the author’s file as it
was, and a change rewrites only the blocks it touches (`an.ir.sync.merge_markdown`).

### Functions

| [`patch_shot_duration_md`](_autosummary/an.stores.scenes.html.md#an.stores.scenes.patch_shot_duration_md)(markdown, shot_id, ...)   | `markdown` with shot `shot_id`'s `duration:` set to `seconds`, every other line — prose, comments, formatting — untouched.   |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|

### Classes

| [`ScenesStore`](_autosummary/an.stores.scenes.html.md#an.stores.scenes.ScenesStore)(project_dir)   | `MutableMapping` exposing the scene file pair under a project root.   |
|-----------------------------------------------------------------------------|-----------------------------------------------------------------------|

### *class* an.stores.scenes.ScenesStore(project_dir)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

`MutableMapping` exposing the scene file pair under a project root.

Keys: currently always `"main"`. The store enforces this by raising
`KeyError` for other keys.

#### patch_shot_durations(durations)

Set `duration` on the named shots WITHOUT regenerating `scene.md`.

`__setitem__` rewrites the markdown from the IR, which drops every word
of prose and every comment the IR does not hold. This writes the JSON
through the read boundary as usual, and patches only each shot’s
`duration:` line in the markdown (inserting one into its

```
``
```

\`\` ``yaml shot ``` block, or the block itself, when there is none) —
what `an sync --accept-measured` uses to take a measured duration into
the authored scene (an#279).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.stores.scenes.patch_shot_duration_md(markdown, shot_id, seconds)

`markdown` with shot `shot_id`’s `duration:` set to `seconds`,
every other line — prose, comments, formatting — untouched.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> md = "# T\n\nA note.\n\n## Shot a (manim)\n\n```yaml shot\noptions: {source: a}  # the chart\n```\n"
>>> print(patch_shot_duration_md(md, "a", 3.1))
# T

A note.

## Shot a (manim)

```yaml shot
duration: 3.1
options: {source: a}  # the chart
```

```text
<BLANKLINE>
```


# _autosummary/an.stores.sounds.html.md

# an.stores.sounds

Sounds store — one directory per sound: `sound.json` beside `audio.wav`.

A sound is an asset like a character or a prop: bytes that end up in a video a
user ships, so it carries its provenance and licence (`source`, an
`an.ir.assets.AssetSource`) and the digest of the bytes the licence is attached
to. The metadata is the mapping’s value; the audio is a sidecar read and written
through `SoundsStore.read_audio()` / `SoundsStore.write_audio()`, so no
caller builds a path by hand (pillar 7). `an.sounds` is the typed front door.

### Classes

| [`SoundsStore`](_autosummary/an.stores.sounds.html.md#an.stores.sounds.SoundsStore)(root_dir)   | Per-sound directory store.   |
|--------------------------------------------------------------------------|------------------------------|

### *class* an.stores.sounds.SoundsStore(root_dir)

Bases: `JsonSidecarStore`

Per-sound directory store.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = SoundsStore(d)
...     store['hit'] = {'description': 'a stick on a table'}
...     store.write_audio('hit', b'RIFF....')
...     store['hit']['description'], store.read_audio('hit')
('a stick on a table', b'RIFF....')
```

#### AUDIO_NAME *= 'audio.wav'*

`an.sounds` reads
its header for the duration a fade-out needs, deterministically.

* **Type:**
  The sidecar holding the audio bytes. WAV only in v1


# _autosummary/an.stores.sources.html.md

# an.stores.sources

The project’s opaque scene sources: files a whole-shot renderer runs as they are.

A Manim shot (`Shot(renderer="manim", options={"source": "chart"})`) names a
key here; the file is `assets/sources/chart.py`. The store is the ONE place
the renderer reads it from (pillar 7), and what it reads is what its shot-cache
key digests, so an edit to the file re-renders the shot.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = SourcesStore(d)
...     store["chart"] = b"from manim import *\n"
...     list(store), store.path_of("chart").name
(['chart'], 'chart.py')
```

### Classes

| [`SourcesStore`](_autosummary/an.stores.sources.html.md#an.stores.sources.SourcesStore)(root_dir)   | Python scene sources (`.py` bytes) — a Manim scene file per key.   |
|---------------------------------------------------------------------------|--------------------------------------------------------------------|

### *class* an.stores.sources.SourcesStore(root_dir)

Bases: `_BlobStore`

Python scene sources (`.py` bytes) — a Manim scene file per key.


# _autosummary/an.stores.styles.html.md

# an.stores.styles

Styles store — visual style presets (color palette, line weight, fonts).

### Classes

| [`StylesStore`](_autosummary/an.stores.styles.html.md#an.stores.styles.StylesStore)(root_dir)   | Pure-JSON style descriptors.   |
|--------------------------------------------------------------------------|--------------------------------|

### *class* an.stores.styles.StylesStore(root_dir)

Bases: `JsonDirStore`

Pure-JSON style descriptors.


# _autosummary/an.stores.voices.html.md

# an.stores.voices

Voices store — pure JSON; one entry per voice.

A voice descriptor carries: provider (e.g. `elevenlabs`), provider voice id,
display name, optional cloning source ref, and emotion presets.

### Classes

| [`VoicesStore`](_autosummary/an.stores.voices.html.md#an.stores.voices.VoicesStore)(root_dir)   | JSON-only voice descriptors.   |
|--------------------------------------------------------------------------|--------------------------------|

### *class* an.stores.voices.VoicesStore(root_dir)

Bases: `JsonDirStore`

JSON-only voice descriptors.

```pycon
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
...     store = VoicesStore(d)
...     store['maya-warm'] = {'provider': 'elevenlabs', 'voice_id': 'xyz'}
...     'maya-warm' in store
True
```


# _autosummary/an.styles.html.md

# an.styles

StylePack: art direction as a document, and the first reader the styles store has had.

The `styles` store was nine lines with no consumer — `rg '["styles"]'` found
only the mall wiring — while colour lived in three disconnected places: the
compiler’s `_CHARACTER_PALETTES`, six literals inside `runtime.js`, and the
character factory, which carried *two disagreeing* palette tables. an#106
retired `AssetRef(kind="style")` because it selected nothing. This is what the
word was reserved for.

**A pack recolours SVG art only where the art says what its colours are.**
The character factory records, per part, which literal it drew as which role
(`CharacterDescriptor.colour_roles`, see `an.characters.colour_roles`),
and the compiler rewrites exactly those literals into a new, content-addressed
inline texture — palette swapping. Untagged art (hand-drawn, DiceBear) is left
alone and the compiler warns once, naming it: a pack would otherwise have to
infer a role from a pixel, which is what produced an#99’s wrong-tone lid. So
the four reasons this module once gave for never touching SVG reduce to one
that still holds — no tags, no recolour. (The texture is inline, so staging and
content addressing are unaffected; the substitution rewrites paint attributes
only, never geometry or ids.)

**A pack must not declare a role it cannot change.** `lip`, `mouth_fill`,
`teeth`, `tongue` and the eye’s white are literals inside `runtime.js`; a role
that resolves to nothing is worse than an absent one, and [`UNREACHABLE_ROLES`](_autosummary/an.styles.html.md#an.styles.UNREACHABLE_ROLES)
plus its test is what keeps the list honest.

\*\*No `line.width`.\*\* A first draft carried one, and nothing read it: the
procedural rig’s stroke is a `runtime.js` literal and an SVG rig’s is inside
its drawing, so the field was written, serialized, and consumed nowhere — the
same shape this module refuses in `UNREACHABLE_ROLES`, and a rule is not a rule
if it exempts the module that states it. It comes back when something reads it.

Colours are **hex strings**, deliberately not DTCG colour objects:
`bench/palette.py` mirrors `runtime.js` verbatim, and a second colour
representation doubles the surface on which the two can silently diverge.

**Surface treatments** (an#163 gap 5) are the pack’s second job: an outline, a
paper-gap drop shadow and a glow per drawable entity ([`SurfaceTreatment`](_autosummary/an.styles.html.md#an.styles.SurfaceTreatment),
`surface` with a per-entity `entity_surfaces` override), and one static
paper grain over the frame ([`Grain`](_autosummary/an.styles.html.md#an.styles.Grain)). Every one is a COMPILE-TIME
expansion into ordinary document content — underlay copies of a part’s own
visual, a gradient sprite, a seeded noise tile — never a runtime filter, and
never anything random at render time. `an.stage.surface` does the
expanding. The outline and the shadow reach ANY SVG art, role-tagged or not:
they copy a part’s texture rather than recolour it. Every width and offset is in
the rig’s own pixels, so a treatment scales with the character like paper would.

### Module Attributes

| [`REACHABLE_ROLES`](_autosummary/an.styles.html.md#an.styles.REACHABLE_ROLES)   | Roles a pack can actually change, because the COMPILER decides them and stamps them into the document the runtime draws.   |
|--------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| [`UNREACHABLE_ROLES`](_autosummary/an.styles.html.md#an.styles.UNREACHABLE_ROLES) | Roles a pack must NOT declare, with what makes each unreachable.                                                           |

### Functions

| [`resolve_palette`](_autosummary/an.styles.html.md#an.styles.resolve_palette)(pack, entity, default)   | `(skin, clothing, hair)` for one entity under `pack`.          |
|-------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`surface_for`](_autosummary/an.styles.html.md#an.styles.surface_for)(pack, entity)                | The treatments `entity` gets under `pack`, or `None` for none. |

### Classes

| [`StylePack`](_autosummary/an.styles.html.md#an.styles.StylePack)(\*\*data)        | Art direction for a project.                                             |
|-----------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`Outline`](_autosummary/an.styles.html.md#an.styles.Outline)(\*\*data)          | A darker, dilated copy drawn behind each part.                           |
| [`PaperShadow`](_autosummary/an.styles.html.md#an.styles.PaperShadow)(\*\*data)      | The "no-platen" paper-gap shadow: an offset, darkened copy of each part. |
| [`Glow`](_autosummary/an.styles.html.md#an.styles.Glow)(\*\*data)             | An additive radial-gradient sprite behind an entity.                     |
| [`Grain`](_autosummary/an.styles.html.md#an.styles.Grain)(\*\*data)            | One static paper-grain texture over the whole frame.                     |
| [`SurfaceTreatment`](_autosummary/an.styles.html.md#an.styles.SurfaceTreatment)(\*\*data) | Which treatments an entity gets.                                         |

### *class* an.styles.Glow(\*\*data)

Bases: `_Treatment`

An additive radial-gradient sprite behind an entity.

Its box is the entity’s drawn box grown by `radius`, and the gradient is
an ELLIPSE over that box: it holds `intensity` out to the entity’s box
along its shorter axis and fades to nothing at the edge — so on a tall
entity the halo is fainter at the top and bottom than at the sides.
Drawn with the engine’s native ADD blend (PixiJS 7 does it in the blend
equation, no filter), as the entity’s first child, so it moves with the
entity and lights the background around it, not the entity itself.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.styles.Grain(\*\*data)

Bases: `_Treatment`

One static paper-grain texture over the whole frame.

Seeded noise generated at COMPILE time (`an.stage.surface`),
tiled on the camera-immune overlay under any text, and MULTIPLIED onto the
frame — so it only ever darkens, by at most `amount`. The same seed is the
same grain on every frame and every machine; nothing is random at render
time.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.styles.Outline(\*\*data)

Bases: `_Treatment`

A darker, dilated copy drawn behind each part.

On a procedural part (rect, ellipse) it is the part’s own shape grown by
`width` — exact for a rect (rounded corners of radius `width`, which is
what dilating by a disk gives) and, for an ellipse, the ellipse with both
radii grown (exact on the axes and for circles). On an SVG part it is a
ring of copies of the part’s texture offset by `width` in
`an.stage.surface.OUTLINE_RING` directions, drawn in `color`
as a `tint`: tint MULTIPLIES, so the outline is exactly `color` where the
art is white and darker elsewhere — exact everywhere for black, and within a
few levels of it for the default near-black.

`nested` extends it to parts nested inside another part (face features
on the head). Off by default: the pieces of a cut-out are the paper; the
face is drawn on them. A procedural eye or mouth never gets one (they are
not copyable shapes); an SVG rig’s eyes and mouth do, under `nested`.

**Fading a treated part darkens it.** The copies are opaque and drawn
separately, so at `alpha` 0.5 the part shows its outline colour through
itself rather than the background. A group fade needs the subtree drawn to
a texture first (a filter), which this package refuses; the compiler warns
when an `alpha` channel reaches a treated part.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* an.styles.PaperShadow(\*\*data)

Bases: `_Treatment`

The “no-platen” paper-gap shadow: an offset, darkened copy of each part.

The copy sits in the part’s own container, so it follows every tween,
`play` and swap of the part with no channel of its own. The offset is
therefore in the PART’s frame: a part that rotates takes its shadow round
with it (a light fixed to the paper, not the room) — invisible at the
small offsets this is for, and a limit to know for a large one.

With an outline, the shadow is grown by the outline width so it shows past
the outline rather than hiding under it: exactly the outlined silhouette on
a procedural part; on an SVG part one copy scaled about the art’s centre, so
it grows the art’s BOX by the width (one copy, because translucent copies
would compound where they overlap).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.styles.REACHABLE_ROLES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'accessory', 'clothing', 'ground', 'hair', 'leg', 'pupil', 'skin', 'sky', 'stroke'})*

Roles a pack can actually change, because the COMPILER decides them and
stamps them into the document the runtime draws.

`skin`, `clothing` and `hair` are `_CHARACTER_PALETTES`’ three components;
`leg` and `pupil` are the compiler’s own literals (`DFLT_LEG_COLOUR`,
`DFLT_PUPIL_COLOUR`); `sky` and `ground` are the environment presets’;
`stroke` is a stroked path’s default colour (`an.stage.paths.DFLT_STROKE_COLOUR`,
an#161) — the arrowhead is filled in the same colour, so it is not a second
role. A path that names its own `color` is art and is left alone.
`accessory` (a hat, a sash) exists only in role-tagged SVG art — the
factory’s `colour_roles` — and reaches the document through the recoloured
texture, as do the SVG rig’s `skin`, `clothing`, `hair`, `leg` and `pupil`.

Every one of these is compiled with a marker colour and asserted to reach
the document by `tests/test_styles.py`. `pupil` shipped in this set wired to
NOTHING (an#112 review) because the guard checked set membership against the
set it was checking — a declared-reachable role that reaches nothing is the
same defect as an unreachable one, and it needs the same kind of test.

### *class* an.styles.StylePack(\*\*data)

Bases: `BaseModel`

Art direction for a project. Saved in the `styles` store.

```pycon
>>> pack = StylePack(name="noir", roles={"skin": "#d8d8d8", "clothing": "#202028"})
>>> pack.colour_for("skin")
'#d8d8d8'
>>> pack.colour_for("hair") is None
True
```

Per-entity overrides win over roles, which is what makes a pack usable on a
scene where one character must stay off-palette:

```pycon
>>> pack = StylePack(name="noir", roles={"skin": "#d8d8d8"},
...                  entities={"maya": {"skin": "#f4c89a"}})
>>> pack.colour_for("skin", entity="maya"), pack.colour_for("skin", entity="bob")
('#f4c89a', '#d8d8d8')
```

A role the renderer cannot reach is refused at construction, not ignored:

```pycon
>>> StylePack(name="x", roles={"lip": "#800000"})
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: ...
```

#### colour_for(role, , entity=None)

The colour for `role`, or `None` when the pack does not set it.

`None` rather than a default: the caller holds today’s literal, and a
pack that does not mention a role must leave it exactly as it was —
which is what keeps a scene with no pack byte-identical.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### entities *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]]*

`{entity id: {role: "#rrggbb"}}` — a per-entity override of `roles`.

#### entity_surfaces *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [SurfaceTreatment](_autosummary/an.styles.html.md#an.styles.SurfaceTreatment)]*

`{entity id: SurfaceTreatment}` — per-entity, key-by-key override of
`surface`.

#### gradient_for(role)

The pack’s gradient for `role`, or `None` (the plane keeps its own paint).

* **Return type:**
  [`Gradient`](_autosummary/an.paint.html.md#an.paint.Gradient) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> pack = StylePack(name="reiniger", gradients={"glass": {
...     "type": "radial", "stops": ["#fff4d6", "#e0a050"]}})
>>> pack.gradient_for("glass").type, pack.gradient_for("sky"), pack.gradient_for(None)
('radial', None, None)
```

#### gradients *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Gradient](_autosummary/an.paint.html.md#an.paint.Gradient)]*

`{role: Gradient}`. A stage plane that
names a `role` (`PlaneArt.role`) is drawn with the pack’s gradient
for it – how a style paints every environment’s backdrop as backlit
glass or a dusk sky without editing the environments. Role names are the
environments’ own, so they are free-form; a role no plane names simply
paints nothing in that scene.

* **Type:**
  **Gradient roles** (an#275)

#### grain *: [Grain](_autosummary/an.styles.html.md#an.styles.Grain) | [None](https://docs.python.org/3/builtins/constants.html#None)*

One static paper grain over the frame; `None` = none.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'allow', 'populate_by_name': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### policy *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

per aspect, the
methods it prefers, in order (`{"locomotion": ["loco.bounce"]}`: this
show bounces even when its characters have legs). The first that
applies wins; a shot’s own `policy` comes first, an author’s request
before both. `None` (the default) leaves every aspect to its chain,
and is not serialized, so a pack without one dumps as before.

* **Type:**
  The style’s **policy** (ADR 0002 decision 4, an#348)

#### roles *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

`{role: "#rrggbb"}`. Hex strings, not colour objects — see the module
docstring for why a second representation is a liability here.

#### surface *: [SurfaceTreatment](_autosummary/an.styles.html.md#an.styles.SurfaceTreatment) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Surface treatments for every drawable entity (characters and props);
`None` = none. See [`SurfaceTreatment`](_autosummary/an.styles.html.md#an.styles.SurfaceTreatment) and [`surface_for()`](_autosummary/an.styles.html.md#an.styles.surface_for).

### *class* an.styles.SurfaceTreatment(\*\*data)

Bases: `_Treatment`

Which treatments an entity gets. Each is off when absent.

In `StylePack.entity_surfaces` an entry OVERRIDES the pack’s `surface`
key by key, for the keys it sets: `{"glow": {...}}` adds a glow and keeps
the pack’s outline; `{"outline": false}` removes the outline. Which keys
were set survives a dump (only they are serialized), so a pack written with
`model_dump()` and read back means the same thing. `null` switches one
off too, but a dump with `exclude_none=True` drops it — and the override
then silently inherits the pack’s treatment — so `false` is the spelling
to store.

```pycon
>>> SurfaceTreatment(glow={}).model_dump()
{'glow': {'color': '#fff4c2', 'radius': 60.0, 'intensity': 0.5}}
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid'}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### an.styles.UNREACHABLE_ROLES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'eye_sclera': 'runtime.js draws the eye white as a literal 0xffffff in makeEye', 'lip': "cutan's runtime script \`_LIP_COLOR\`, drawn by makeMouth and never read from the document", 'mouth_fill': "cutan's runtime script \`_MOUTH_FILL\`", 'teeth': 'runtime.js \`_TEETH_COLOR\`', 'tongue': 'runtime.js \`_TONGUE_COLOR\`'}*

Roles a pack must NOT declare, with what makes each unreachable. These are
`runtime.js` literals: `_LIP_COLOR`, `_MOUTH_FILL`, `_TEETH_COLOR`,
`_TONGUE_COLOR`, and the eye white’s `0xffffff` — none of them read anything
from the compiled document, so a pack that named them would be accepted,
change nothing, and say nothing. `tests/test_styles.py` reads the literals
out of `runtime.js` so this list cannot quietly stop matching it.

### an.styles.resolve_palette(pack, entity, default)

`(skin, clothing, hair)` for one entity under `pack`.

A **lookup with a default**, not a rewrite: with no pack, or with a pack
that mentions none of the three, the caller’s own literals come back
unchanged and the compiled document does not move a byte.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> resolve_palette(None, "maya", ("#f4c89a", "#3a6ea5", "#3b2a1a"))
('#f4c89a', '#3a6ea5', '#3b2a1a')
>>> pack = StylePack(name="noir", roles={"clothing": "#202028"})
>>> resolve_palette(pack, "maya", ("#f4c89a", "#3a6ea5", "#3b2a1a"))
('#f4c89a', '#202028', '#3b2a1a')
```

### an.styles.surface_for(pack, entity)

The treatments `entity` gets under `pack`, or `None` for none.

`None` is the answer for no pack, a pack without treatments, and an
entity whose override switched everything off — which is what keeps every
such scene’s compiled document byte-identical to before an#163.

* **Return type:**
  [`SurfaceTreatment`](_autosummary/an.styles.html.md#an.styles.SurfaceTreatment) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> pack = StylePack(name="sp", surface={"outline": {}},
...                  entity_surfaces={"sun": {"glow": {}}, "bob": {"outline": False}})
>>> sorted(surface_for(pack, "sun").model_dump())
['glow', 'outline']
>>> surface_for(pack, "bob") is None
True
>>> surface_for(pack, "maya").outline.width
3.0
>>> surface_for(None, "maya") is None
True
```


# _autosummary/an.text.html.md

# an.text

Moved to [`an.stage.text`](_autosummary/an.stage.text.html.md#module-an.stage.text) (an#247); this path is a LIVE alias of it.

The 2D stage runtime is the first engine, outside the core’s import firewall
(ADR 0001 decisions 5 and 14). Every name of the new module is reachable here,
and rebinding one here (a bench lever, `monkeypatch`) rebinds it there, where
the code runs (`an._shims.alias_module()`).


# _autosummary/an.timing.address.html.md

# an.timing.address

The address grammar: one way to name an animatable value, in every genre.

```default
<entity>[/<node>…]:<field>[@<qualifier>]
```

- `entity` — what the scene holds (a character, a prop, the camera, a view);
- `/<node>…` — a path inside it (`charlie/left_arm`); `root` is the
  stage’s reserved scene root, which the camera lowers onto;
- `field` — a property the entity kind’s property space declares
  ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)). It may be a **dotted** path (`view:light.color`),
  and a dotted segment names a declared field, never a member *inside* a
  composite value: an `orbit` interpolates as one value, so
  `view:camera.azimuth` is addressable only if the space declares
  `camera.azimuth` itself;
- `@<qualifier>` — a variant of the field (`mouth:viseme@happy`, a variant
  swap set), which the stage already emits.

The compiled form’s `(target, property)` pair is exactly
`(entity/nodes, field@qualifier)`, and the golden vectors key every state by
the address string.

```pycon
>>> a = parse_address("charlie/head/mouth:viseme@happy")
>>> a.entity, a.nodes, a.field, a.qualifier
('charlie', ('head', 'mouth'), 'viseme', 'happy')
>>> a.target, a.property
('charlie/head/mouth', 'viseme@happy')
>>> str(parse_address("view:light.color")), parse_address("view:light.color").field_path
('view:light.color', ('light', 'color'))
>>> str(Address.of("root", "pivot_x"))
'root:pivot_x'
>>> parse_address("charlie:")
Traceback (most recent call last):
 ...
an.timing.address.AddressError: 'charlie:': the field is empty
```

### Module Attributes

| [`ENTITY_FIELD_SEP`](_autosummary/an.timing.address.html.md#an.timing.address.ENTITY_FIELD_SEP)   | Separators of the grammar; none of them may appear inside a name.   |
|---------------------------------------------------------------------|---------------------------------------------------------------------|

### Functions

| [`format_address`](_autosummary/an.timing.address.html.md#an.timing.address.format_address)(target, prop)   | The address string of a compiled `(target, property)` pair, unvalidated (the fast path for keying states; [`Address.of()`](_autosummary/an.timing.address.html.md#an.timing.address.Address.of) validates).   |
|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`parse_address`](_autosummary/an.timing.address.html.md#an.timing.address.parse_address)(text)            | `text` as an [`Address`](_autosummary/an.timing.address.html.md#an.timing.address.Address); raises [`AddressError`](_autosummary/an.timing.address.html.md#an.timing.address.AddressError) naming the fault.                         |

### Classes

| [`Address`](_autosummary/an.timing.address.html.md#an.timing.address.Address)(entity[, nodes, field, qualifier])   | A parsed address.   |
|-----------------------------------------------------------------------------------------------|---------------------|

### Exceptions

| [`AddressError`](_autosummary/an.timing.address.html.md#an.timing.address.AddressError)   | A string is not an address under the grammar.   |
|-----------------------------------------------------------------|-------------------------------------------------|

### *class* an.timing.address.Address(entity, nodes=(), field='', qualifier=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A parsed address. `str(address)` writes it back unchanged.

#### *property* field_path *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

The dotted field, split.

#### *classmethod* of(target, prop)

The address of a compiled `(target, property)` pair (validated).

* **Return type:**
  [`Address`](_autosummary/an.timing.address.html.md#an.timing.address.Address)

#### *property* property *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

the field and its qualifier.

* **Type:**
  The compiled form’s `property`

#### *property* target *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

the entity and its node path.

* **Type:**
  The compiled form’s `target`

### *exception* an.timing.address.AddressError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A string is not an address under the grammar.

### an.timing.address.ENTITY_FIELD_SEP *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= ':'*

Separators of the grammar; none of them may appear inside a name.

### an.timing.address.format_address(target, prop)

The address string of a compiled `(target, property)` pair, unvalidated
(the fast path for keying states; [`Address.of()`](_autosummary/an.timing.address.html.md#an.timing.address.Address.of) validates).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.timing.address.parse_address(text)

`text` as an [`Address`](_autosummary/an.timing.address.html.md#an.timing.address.Address); raises [`AddressError`](_autosummary/an.timing.address.html.md#an.timing.address.AddressError) naming the fault.

* **Return type:**
  [`Address`](_autosummary/an.timing.address.html.md#an.timing.address.Address)

```pycon
>>> parse_address("charlie/left_arm:rotation").target
'charlie/left_arm'
>>> parse_address("a::b")
Traceback (most recent call last):
 ...
an.timing.address.AddressError: 'a::b': an address has exactly one ':', found 2
```


# _autosummary/an.timing.channel.html.md

# an.timing.channel

Channel: keyframes for a single (target, property) pair, evaluated at time t.

A channel holds a sorted list of `Keyframe`s. ``evaluate(channel, t)`` does a
binary search to find the surrounding keyframes, applies the easing for that
segment, and interpolates between the two values. Keys are half-open: the
segment `[a.time, b.time)` belongs to `a`; before the first key the first
value holds, from the last key on the last value holds; a zero-span segment
(two keys at one time) resolves to the later key.

**Who picks the interpolator.** Two rules, one per caller:

- `kind=None` — **by value type**, the rule `runtime.js`’s
  `evaluateChannel` implements, and the default because this function is that
  port’s executable spec (`tests/test_cutout_channel_parity.py` runs the real
  extracted JS against it). Numbers (`int`/`float`, excluding `bool`)
  interpolate through the segment’s easing; everything else holds `a` for
  exactly `[a.time, b.time)` and switches at `b.time`. Being runtime.js’s
  rule, it accepts only runtime.js’s easings
  ([`VALUE_TYPED_EASINGS`](_autosummary/an.timing.easing.html.md#an.timing.easing.VALUE_TYPED_EASINGS)): a curve the stage cannot
  draw raises here, as it does in the browser, instead of yielding a pose.
- `kind=<FieldKind>` — **by declaration** ([`an.timing.kinds`](_autosummary/an.timing.kinds.html.md#module-an.timing.kinds)), the
  kernel contract’s rule: the declared kind interpolates, a discrete kind
  switches on time, and the first instant of a segment is the key it leaves.
  The stage’s declarations (`stage.node`) give the same values as the
  value-type rule on everything the stage compiler emits; a test holds that.

In both, the snap of a held value compares `t` against a TIME, never an eased
or derived parameter, because each indirection was measured wrong: an
overshooting cubic-bezier easing crosses 1.0 mid-segment (showing the *second*
key early, or flapping A→B→A within one segment), and even the raw
`(t - a.time) / span` can round up to 1.0 while `t < b.time`. And in both,
the easing is *validated* on every segment (an unknown spec raises) so a typo’d
easing name stays loud on a swap channel too.

`bool` keyframe values are refused upstream by the stage compiler: Python’s
`isinstance(True, int)` would lerp what JS’s `typeof` snaps.

```pycon
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> evaluate(ch, 0.5)
5.0
>>> evaluate(ch, -1.0)  # before first → clamps to first value
0.0
>>> evaluate(ch, 99.0)  # after last → clamps to last value
10.0
>>> sw = Channel("a", "hands", [Keyframe(0.0, "fist"), Keyframe(1.0, "open")])
>>> evaluate(sw, 0.999)  # holds the first key for the whole segment
'fist'
>>> evaluate(sw, 1.0)  # switches exactly at the keyframe
'open'
>>> from an.timing.kinds import AngleKind
>>> spin = Channel("a", "yaw", [Keyframe(0.0, 350.0), Keyframe(1.0, 10.0)])
>>> evaluate(spin, 0.5), evaluate(spin, 0.5, kind=AngleKind())
(180.0, 360.0)
```

### Functions

| [`check_channel`](_autosummary/an.timing.channel.html.md#an.timing.channel.check_channel)(channel, kind)     | Why `channel`'s keyframe values do not fit `kind` (empty if they do).   |
|-----------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`evaluate`](_autosummary/an.timing.channel.html.md#an.timing.channel.evaluate)(channel, t, \*[, kind]) | Evaluate `channel` at time `t` (see the module docstring for `kind`).   |

### Classes

| [`Channel`](_autosummary/an.timing.channel.html.md#an.timing.channel.Channel)(target, property[, keyframes])   | Sorted keyframes for one property of one target.        |
|-------------------------------------------------------------------------------------------|---------------------------------------------------------|
| [`Keyframe`](_autosummary/an.timing.channel.html.md#an.timing.channel.Keyframe)(time, value[, easing])          | One keyframe: time, value, optional per-segment easing. |

### *class* an.timing.channel.Channel(target, property, keyframes=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Sorted keyframes for one property of one target.

Construction validates that `keyframes` is non-empty and sorted.

### *class* an.timing.channel.Keyframe(time, value, easing=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One keyframe: time, value, optional per-segment easing.

The easing on a keyframe describes the curve **leaving** that keyframe
toward the next one. The last keyframe’s easing is therefore unused.

### an.timing.channel.check_channel(channel, kind)

Why `channel`’s keyframe values do not fit `kind` (empty if they do).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.timing.channel.evaluate(channel, t, , kind=None)

Evaluate `channel` at time `t` (see the module docstring for `kind`).

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)


# _autosummary/an.timing.clip.html.md

# an.timing.clip

Clip: a named bundle of channels with a duration and loop mode.

A clip is what you’d call an “animation” in Spine / Rive terminology — a
reusable unit (e.g. `"walk_cycle"`, `"wave"`). Evaluating a clip at time
`t` produces a `Pose` by evaluating each of its channels at `t`.

Loop modes:

- `LoopMode.ONCE` — past `duration`, the last frame holds.
- `LoopMode.LOOP` — `t` wraps modulo `duration`.
- `LoopMode.PING_PONG` — `t` ping-pongs over `[0, duration]`.

```pycon
>>> from an.timing.channel import Channel, Keyframe
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> clip = Clip("walk", duration=1.0, channels=[ch], loop_mode=LoopMode.LOOP)
>>> evaluate(clip, 0.5)[("a", "x")]
5.0
>>> evaluate(clip, 1.25)[("a", "x")]  # loop wraps
2.5
```

### Module Attributes

| [`Pose`](_autosummary/an.timing.clip.html.md#an.timing.clip.Pose)                              | Mapping of (target_path, property_name) -> value — the universal output of animation evaluation.                          |
|------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| [`KindOf`](_autosummary/an.timing.clip.html.md#an.timing.clip.KindOf)                            | the declared kind of each channel.                                                                                        |
| [`wrap_time`](_autosummary/an.timing.clip.html.md#an.timing.clip.wrap_time)(t, duration, loop_mode) | The public name of the loop rule (the contract's; `_wrap_time` is kept for the callers and tests that already import it). |

### Functions

| [`evaluate`](_autosummary/an.timing.clip.html.md#an.timing.clip.evaluate)(clip, t, \*[, kind_of])   | Evaluate `clip` at time `t`, returning a `Pose`.                                                                          |
|-------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| [`merge_poses`](_autosummary/an.timing.clip.html.md#an.timing.clip.merge_poses)(\*poses)               | Merge multiple poses with **override semantics** (later wins per key).                                                    |
| [`wrap_time`](_autosummary/an.timing.clip.html.md#an.timing.clip.wrap_time)(t, duration, loop_mode)  | The public name of the loop rule (the contract's; `_wrap_time` is kept for the callers and tests that already import it). |

### Classes

| [`Clip`](_autosummary/an.timing.clip.html.md#an.timing.clip.Clip)(name, duration[, channels, loop_mode])   | Named animation: a duration + a bundle of channels.   |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------|
| [`LoopMode`](_autosummary/an.timing.clip.html.md#an.timing.clip.LoopMode)(\*values)                            | How a clip behaves past its natural duration.         |

### *class* an.timing.clip.Clip(name, duration, channels=<factory>, loop_mode=LoopMode.ONCE)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Named animation: a duration + a bundle of channels.

### an.timing.clip.KindOf

the declared kind of each channel.

* **Type:**
  `(target, property) -> FieldKind`

alias of `Callable`[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)]

### *class* an.timing.clip.LoopMode(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

How a clip behaves past its natural duration.

### an.timing.clip.Pose

Mapping of (target_path, property_name) -> value — the universal output of
animation evaluation. Application happens in `runtime.js` (`applyPose`);
the Python side only ever *produces* poses (an#86 deleted the Python
applier, which structurally could not apply swap or alpha values).

alias of [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.clip.evaluate(clip, t, , kind_of=None)

Evaluate `clip` at time `t`, returning a `Pose`.

`kind_of` declares each channel’s field kind; `None` interpolates by
value type, as `runtime.js` does (see [`an.timing.channel`](_autosummary/an.timing.channel.html.md#module-an.timing.channel)).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.clip.merge_poses(\*poses)

Merge multiple poses with **override semantics** (later wins per key).

Used by the timeline to combine concurrent clips on the same target.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> merge_poses({("a", "x"): 1.0}, {("a", "x"): 2.0, ("a", "y"): 3.0})
{('a', 'x'): 2.0, ('a', 'y'): 3.0}
```

### an.timing.clip.wrap_time(t, duration, loop_mode)

The public name of the loop rule (the contract’s; `_wrap_time` is kept for
the callers and tests that already import it).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)


# _autosummary/an.timing.contract.html.md

# an.timing.contract

The timing kernel’s cross-language contract: five JSON files, generated from the registries.

The contract lives in `an/data/timing/` (shipped in the wheel) and is what any
other implementation of the kernel — the TypeScript one, a package that copies
`an`’s track format — copies and asserts:

| `easing.json`          | every registered easing (name, version, family,<br/>solver) with sample values, plus parametrised examples   |
|------------------------|--------------------------------------------------------------------------------------------------------------|
| `kinds.json`           | every field kind with sample interpolations, and the<br/>seeded property spaces                              |
| `timeline.schema.json` | the authored flat timeline `(start, end, address, change)`                                                   |
| `compiled.schema.json` | tracks, placed clips, loop modes, channels — the<br/>compiled form `runtime.js` consumes                     |
| `timing_vectors.json`  | compiled documents and the state at listed times                                                             |

**Numbers agree within** `1e-9 * max(1, |x|, |y|)`; strings, booleans,
`null`, list lengths and object keys agree exactly. A state is SPARSE: an
address no clip has started writing is absent (at rest).

The files are regenerated by `python -m an.timing.contract write` and checked
by `python -m an.timing.contract check` (a test runs the check). Regenerating
is a deliberate act: a change to a committed sample is a change of the contract.

```pycon
>>> doc = {"timeline": {"duration": 1.0, "tracks": [{"clips": [
...     {"animation_id": "m", "start_time": 0.0}]}]},
...     "animations": {"m": {"duration": 1.0, "channels": [{"target": "cam",
...     "property": "yaw", "keyframes": [{"time": 0.0, "value": 350.0},
...     {"time": 1.0, "value": 10.0}]}]}}}
>>> space = {"fields": [{"pattern": "yaw", "spec": {"kind": "angle"}}]}
>>> state_at(doc, 0.5, space=space)
{'cam:yaw': 360.0}
```

### Module Attributes

| [`CONTRACT_DIR`](_autosummary/an.timing.contract.html.md#an.timing.contract.CONTRACT_DIR)           | Where the committed contract files live (inside the package, so they ship).                                                                                                                                                                                          |
|-------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`REL_TOL`](_autosummary/an.timing.contract.html.md#an.timing.contract.REL_TOL)                | ```<br/>|x - y|<br/>```<br/><br/> <= REL_TOL \* max(1, <br/><br/>```<br/>|x|<br/>```<br/><br/>, <br/><br/>```<br/>|y|<br/>```<br/><br/>).                                                                                                                            |
| [`VALUE_TYPED_CASE_SPACE`](_autosummary/an.timing.contract.html.md#an.timing.contract.VALUE_TYPED_CASE_SPACE) | The cases whose space is the value-typed rule's write-group space are ALSO evaluated with `space=VALUE_TYPED` — the rule `runtime.js` implements — so both rules are held to the same numbers (the default, `space=None`, is the declared rule since an#239 item 2). |
| [`SOLVER_PROBE_U`](_autosummary/an.timing.contract.html.md#an.timing.contract.SOLVER_PROBE_U)         | a uniform grid, plus points that reach the solvers' fallbacks (near the ends, and around the middle of a curve whose slope vanishes there, e.g. cubic-bezier(1, 0, 0, 1)).                                                                                           |
| [`PARAMETRIC_EXAMPLES`](_autosummary/an.timing.contract.html.md#an.timing.contract.PARAMETRIC_EXAMPLES)    | Parametrised specs the easing file samples, beside the named entries.                                                                                                                                                                                                |
| [`CSS_IMPORT_ALIASES`](_autosummary/an.timing.contract.html.md#an.timing.contract.CSS_IMPORT_ALIASES)     | How a CSS-named document's names map onto this registry where the name means something else here.                                                                                                                                                                    |
| [`ADDRESS_PATTERN`](_autosummary/an.timing.contract.html.md#an.timing.contract.ADDRESS_PATTERN)        | The address grammar as a regular expression (the parser is `an.timing.address`).                                                                                                                                                                                     |

### Functions

| `build_compiled_schema`()                                                                      |                                                                                                                |
|------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| [`build_contract`](_autosummary/an.timing.contract.html.md#an.timing.contract.build_contract)()                              | Every contract file's content, by file name, generated from the registries.                                    |
| `build_easing`()                                                                               |                                                                                                                |
| `build_kinds`()                                                                                |                                                                                                                |
| `build_timeline_schema`()                                                                      |                                                                                                                |
| `build_vectors`()                                                                              |                                                                                                                |
| [`check_vectors`](_autosummary/an.timing.contract.html.md#an.timing.contract.check_vectors)([vectors])                      | Every sample of `vectors` (default: the committed file) that `an.timing` does not reproduce, as one line each. |
| [`contract_drift`](_autosummary/an.timing.contract.html.md#an.timing.contract.contract_drift)([directory])                   | How the committed files differ from what the registries generate today.                                        |
| [`dumps`](_autosummary/an.timing.contract.html.md#an.timing.contract.dumps)(obj)                                    | The committed text of a contract file (stable, diffable).                                                      |
| `load_contract_file`(name, \*[, directory])                                                    |                                                                                                                |
| [`main`](_autosummary/an.timing.contract.html.md#an.timing.contract.main)([argv])                                  | `python -m an.timing.contract [write|check]`.                                                                  |
| [`resolve_space`](_autosummary/an.timing.contract.html.md#an.timing.contract.resolve_space)(space)                          | A vector case's `space`: a registered space's name, or its JSON form.                                          |
| [`state_at`](_autosummary/an.timing.contract.html.md#an.timing.contract.state_at)(doc, t, \*, space)                   | The sparse state of compiled document `doc` at `t`, keyed by address.                                          |
| [`values_close`](_autosummary/an.timing.contract.html.md#an.timing.contract.values_close)(expected, actual, \*[, rel_tol]) | The contract's comparison (see the module docstring).                                                          |
| [`write_contract`](_autosummary/an.timing.contract.html.md#an.timing.contract.write_contract)([directory])                   | Regenerate every contract file under `directory`.                                                              |

### an.timing.contract.ADDRESS_PATTERN *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '^[^:/@\\\\s](?:[^:/@]\*[^:/@\\\\s])?(?:/[^:/@\\\\s](?:[^:/@]\*[^:/@\\\\s])?)\*:[^:/@.\\\\s](?:[^:/@.]\*[^:/@.\\\\s])?(?:\\\\.[^:/@.\\\\s](?:[^:/@.]\*[^:/@.\\\\s])?)\*(?:@[^:/@\\\\s](?:[^:/@]\*[^:/@\\\\s])?)?$'*

The address grammar as a regular expression (the parser is `an.timing.address`).

### an.timing.contract.CONTRACT_DIR *: [Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path)* *= PosixPath('/home/runner/work/an/an/an/data/timing')*

Where the committed contract files live (inside the package, so they ship).

### an.timing.contract.CSS_IMPORT_ALIASES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'ease': 'cubic-bezier(0.25, 0.1, 0.25, 1)'}*

How a CSS-named document’s names map onto this registry where the name means
something else here. `ease` is `an`’s quadratic (ADR 0001 decision 10,
amended 2026-10-01); a CSS document’s `ease` is this spec.

### an.timing.contract.PARAMETRIC_EXAMPLES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Any](https://docs.python.org/3/library/typing.html#typing.Any), ...]* *= ('cubic-bezier(0.25, 0.1, 0.25, 1)', 'cubic-bezier(0.5, -0.8, 0.5, 1.8)', 'cubic-bezier(0.3, 1.8, 0.7, -0.8)', 'steps(4)', 'steps(4, jump-start)', 'steps(3, jump-none)', 'steps(2, jump-both)', 'steps(2, start)', 'cubic-bezier(1, 0, 0, 1)', [0.42, 0.0, 0.58, 1.0], [0.5, 2.0, 0.5, 2.0], [0.3, 3.0, 0.7, 0.0], [1.0, 0.0, 0.0, 1.0], [0.42, 0.0, 1.0, 1.0], [0.0, 0.0, 0.58, 1.0])*

Parametrised specs the easing file samples, beside the named entries.

### an.timing.contract.REL_TOL *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 1e-09*

```
|x - y|
```

 <= REL_TOL \* max(1, 

```
|x|
```

, 

```
|y|
```

).

* **Type:**
  The contract’s tolerance

### an.timing.contract.SOLVER_PROBE_U *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), ...]* *= (1e-06, 0.4, 0.501, 0.999999)*

a uniform grid, plus points that reach the
solvers’ fallbacks (near the ends, and around the middle of a curve whose
slope vanishes there, e.g. cubic-bezier(1, 0, 0, 1)).

* **Type:**
  Where every easing is sampled

### an.timing.contract.VALUE_TYPED_CASE_SPACE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'stage.node'*

The cases whose space is the value-typed rule’s write-group space are ALSO
evaluated with `space=VALUE_TYPED` — the rule `runtime.js` implements —
so both rules are held to the same numbers (the default, `space=None`, is
the declared rule since an#239 item 2).

### an.timing.contract.build_contract()

Every contract file’s content, by file name, generated from the registries.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.contract.check_vectors(vectors=None)

Every sample of `vectors` (default: the committed file) that `an.timing`
does not reproduce, as one line each. Empty means the kernel meets the contract.

Both evaluation rules are held: every case under its declared space, and every
`stage.node` case also under the value-typed rule (`space=VALUE_TYPED`).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.timing.contract.contract_drift(directory=PosixPath('/home/runner/work/an/an/an/data/timing'))

How the committed files differ from what the registries generate today.

Numbers compare with the contract’s tolerance, because a transcendental
function (`exp`, `sin`, `pow`) may differ in its last bit across
platforms’ maths libraries; anything else must match exactly.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.timing.contract.dumps(obj)

The committed text of a contract file (stable, diffable).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.timing.contract.main(argv=None)

`python -m an.timing.contract [write|check]`.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### an.timing.contract.resolve_space(space)

A vector case’s `space`: a registered space’s name, or its JSON form.

* **Return type:**
  [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)

### an.timing.contract.state_at(doc, t, , space)

The sparse state of compiled document `doc` at `t`, keyed by address.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.contract.values_close(expected, actual, , rel_tol=1e-09)

The contract’s comparison (see the module docstring).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> values_close(1e9, 1e9 + 0.5), values_close([1, "a"], [1.0, "a"]), values_close(True, 1)
(True, True, False)
```

### an.timing.contract.write_contract(directory=PosixPath('/home/runner/work/an/an/an/data/timing'))

Regenerate every contract file under `directory`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]


# _autosummary/an.timing.easing.html.md

# an.timing.easing

The easing registry: every named timing curve, each with the solver that computes it.

An **easing** (CSS: *timing function*; Manim: *rate function*) maps a segment’s
normalised time `u` in `[0, 1]` to its progress. One registry holds every
curve a timeline may name, and each entry carries its own **solver**, because two
solvers of “the same” curve do not agree to the contract tolerance (core study
§2.3): `an`’s cubic Bézier runs 8 Newton steps and stops, the CSS one runs Newton
and then bisects.

Four vocabularies live here, and none is silently aliased to another:

- **legacy** — `an`’s own names, with the polynomial curves `an` has always drawn
  (`ease`, `ease_in`, `ease_out`, `ease_in_out`, `step`). `ease` and
  `ease_in_out` are the quadratic ease-in-out, **not** CSS `ease`; the CSS
  `ease` curve is spelled `cubic-bezier(0.25, 0.1, 0.25, 1)`.
- **css** — CSS Easing Level 1/2 hyphenated names with the exact CSS curves
  (`ease-in`, `ease-out`, `ease-in-out`, `step-start`, `step-end`),
  plus the parametrised `cubic-bezier(x1, y1, x2, y2)` and
  `steps(n[, position])`.
- **manim** — Manim Community Edition’s rate functions under their own names
  (`smooth`, `there_and_back`, `rush_into`, …), so a curve means the same
  thing whether the stage engine or Manim draws it.
- **common** — `linear`, which every vocabulary agrees on.

A bare 4-sequence `[cx1, cy1, cx2, cy2]` (what `an` scenes have always written)
is a cubic Bézier solved by the **legacy** solver, so no existing scene moves.

```pycon
>>> apply_easing("linear", 0.5)
0.5
>>> apply_easing("ease_in_out", 0.25)  # an's quadratic, not CSS
0.125
>>> round(apply_easing("ease-in-out", 0.25), 6)  # the CSS curve
0.129162
>>> round(apply_easing("cubic-bezier(0.42, 0, 0.58, 1)", 0.25), 6)
0.129162
>>> apply_easing("steps(4)", 0.3)
0.25
>>> round(apply_easing("smooth", 0.25), 6)  # Manim's default rate function
0.070104
>>> round(apply_easing([0.42, 0.0, 0.58, 1.0], 0.25), 6)  # legacy solver
0.129162
```

### Module Attributes

| [`Curve`](_autosummary/an.timing.easing.html.md#an.timing.easing.Curve)                      | normalised time `u` -> progress.                                                                                                                            |
|-----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`CORE_OWNER`](_autosummary/an.timing.easing.html.md#an.timing.easing.CORE_OWNER)                 | Who registered a core entry.                                                                                                                                |
| [`EASING_FAMILIES`](_autosummary/an.timing.easing.html.md#an.timing.easing.EASING_FAMILIES)            | a genre adds its own (a Penner family, say) with [`register_family()`](_autosummary/an.timing.easing.html.md#an.timing.easing.register_family).                                        |
| [`SOLVERS`](_autosummary/an.timing.easing.html.md#an.timing.easing.SOLVERS)                    | The core solvers, by name.                                                                                                                                  |
| [`LEGACY_BEZIER_NEWTON_STEPS`](_autosummary/an.timing.easing.html.md#an.timing.easing.LEGACY_BEZIER_NEWTON_STEPS) | Newton steps of the legacy Bézier solver, and its degenerate-slope guard.                                                                                   |
| [`CSS_BEZIERS`](_autosummary/an.timing.easing.html.md#an.timing.easing.CSS_BEZIERS)                | The CSS named curves, as cubic-bezier control points (CSS Easing Level 1).                                                                                  |
| [`CSS_NEWTON_STEPS`](_autosummary/an.timing.easing.html.md#an.timing.easing.CSS_NEWTON_STEPS)           | The CSS solver's constants (see `SOLVERS["css-bezier-newton-bisection"]`).                                                                                  |
| [`STEP_POSITIONS`](_autosummary/an.timing.easing.html.md#an.timing.easing.STEP_POSITIONS)             | The positions `steps()` accepts; `start`/`end` are the CSS aliases of `jump-start`/`jump-end`.                                                              |
| [`MANIM_INFLECTION`](_autosummary/an.timing.easing.html.md#an.timing.easing.MANIM_INFLECTION)           | Manim's default sigmoid steepness for `smooth` and its relatives.                                                                                           |
| [`VALUE_TYPED_EASINGS`](_autosummary/an.timing.easing.html.md#an.timing.easing.VALUE_TYPED_EASINGS)        | exactly `runtime.js`'s `EASINGS` table (the stage engine draws these and no others), so the Python spec of that rule refuses what the browser would refuse. |

### Functions

| [`apply_easing`](_autosummary/an.timing.easing.html.md#an.timing.easing.apply_easing)(spec, t, \*[, names])              | Apply an easing spec to a normalised parameter `t` in `[0, 1]`.              |
|--------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`css_cubic_bezier`](_autosummary/an.timing.easing.html.md#an.timing.easing.css_cubic_bezier)(x1, y1, x2, y2)                | The CSS `cubic-bezier(x1, y1, x2, y2)` curve, solved to well below 1e-9.     |
| [`css_steps`](_autosummary/an.timing.easing.html.md#an.timing.easing.css_steps)(n[, position])                        | CSS `steps(n, position)`, following the CSS Easing Level 1 algorithm.        |
| [`easing_entries`](_autosummary/an.timing.easing.html.md#an.timing.easing.easing_entries)(\*[, owner])                     | Every registered entry in registration order; only `owner`'s when given.     |
| [`easing_entry`](_autosummary/an.timing.easing.html.md#an.timing.easing.easing_entry)(name)                              | The registered entry called `name`.                                          |
| [`legacy_cubic_bezier`](_autosummary/an.timing.easing.html.md#an.timing.easing.legacy_cubic_bezier)(cx1, cy1, cx2, cy2, t)      | an's cubic-Bézier easing at `t` — the `an-bezier-newton-8` solver.           |
| [`register_easing`](_autosummary/an.timing.easing.html.md#an.timing.easing.register_easing)(entry, \*[, replace, owner])    | Add `entry` to the registry (a genre's own curves register here).            |
| [`register_family`](_autosummary/an.timing.easing.html.md#an.timing.easing.register_family)(name, \*[, owner])              | Open a new easing family (refused if it exists).                             |
| [`register_solver`](_autosummary/an.timing.easing.html.md#an.timing.easing.register_solver)(name, description, \*[, owner]) | Name a new solver, with the description another language implements it from. |
| [`resolve_easing`](_autosummary/an.timing.easing.html.md#an.timing.easing.resolve_easing)(spec)                            | The curve an easing spec names.                                              |
| [`solvers`](_autosummary/an.timing.easing.html.md#an.timing.easing.solvers)(\*[, owner])                            | The registered solvers; only `owner`'s when given.                           |

### Classes

| [`EasingEntry`](_autosummary/an.timing.easing.html.md#an.timing.easing.EasingEntry)(name, curve, family, solver, ...)   | One named easing: its curve, where it comes from, and how it is solved.   |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|

### Exceptions

| [`UnknownEasingError`](_autosummary/an.timing.easing.html.md#an.timing.easing.UnknownEasingError)   | An easing spec names no registered curve and parses as no parametrised one.   |
|-----------------------------------------------------------------------|-------------------------------------------------------------------------------|

### an.timing.easing.CORE_OWNER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an'*

Who registered a core entry. Registries record an owner for every entry, and
the contract files ([`an.timing.contract`](_autosummary/an.timing.contract.html.md#module-an.timing.contract)) list only this owner’s, so a
genre’s registrations never leak into `an`’s core contract.

### an.timing.easing.CSS_BEZIERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]]* *= {'ease': (0.25, 0.1, 0.25, 1.0), 'ease-in': (0.42, 0.0, 1.0, 1.0), 'ease-in-out': (0.42, 0.0, 0.58, 1.0), 'ease-out': (0.0, 0.0, 0.58, 1.0)}*

The CSS named curves, as cubic-bezier control points (CSS Easing Level 1).

### an.timing.easing.CSS_NEWTON_STEPS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 8*

The CSS solver’s constants (see `SOLVERS["css-bezier-newton-bisection"]`).

### an.timing.easing.Curve

normalised time `u` -> progress.

* **Type:**
  A curve

alias of `Callable`[[[`float`](https://docs.python.org/3/builtins/functions.html#float)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.timing.easing.EASING_FAMILIES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('common', 'legacy', 'css', 'manim')*

a genre adds its own
(a Penner family, say) with [`register_family()`](_autosummary/an.timing.easing.html.md#an.timing.easing.register_family).

* **Type:**
  The core families (see the module docstring). Open

### *class* an.timing.easing.EasingEntry(name, curve, family, solver, description, version=1, params=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One named easing: its curve, where it comes from, and how it is solved.

`version` follows ADR 0003: an entry whose meaning changes gets a new
version, so a shot that names it re-renders visibly instead of silently.

#### to_json()

The entry as the contract file lists it (without samples).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.easing.LEGACY_BEZIER_NEWTON_STEPS *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 8*

Newton steps of the legacy Bézier solver, and its degenerate-slope guard.

### an.timing.easing.MANIM_INFLECTION *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 10.0*

Manim’s default sigmoid steepness for `smooth` and its relatives.

### an.timing.easing.SOLVERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'an-bezier-newton-8': "an's cubic Bézier: x(s) = t solved by exactly 8 Newton steps from s = t, each clamped to [0, 1], stopping only on a derivative below 1e-12; no bisection. Bit-identical to runtime.js cubicBezier", 'closed-form': 'evaluated directly from its formula', 'css-bezier-newton-bisection': 'the CSS cubic Bézier: up to 8 Newton steps from s = t (stop when |x(s) - t| < 1e-12, give up on a slope below 1e-6), then up to 60 bisection steps on [0, 1] to the same 1e-12', 'css-steps': 'the CSS Easing Level 1 step algorithm'}*

The core solvers, by name. An entry names the one that computes it, so a
curve’s numbers are reproducible in another language from its entry alone.
Open: a genre adds one (a spring integrator) with [`register_solver()`](_autosummary/an.timing.easing.html.md#an.timing.easing.register_solver).

### an.timing.easing.STEP_POSITIONS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('jump-start', 'jump-end', 'jump-none', 'jump-both', 'start', 'end')*

The positions `steps()` accepts; `start`/`end` are the CSS aliases of
`jump-start`/`jump-end`.

### *exception* an.timing.easing.UnknownEasingError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

An easing spec names no registered curve and parses as no parametrised one.

### an.timing.easing.VALUE_TYPED_EASINGS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'ease', 'ease_in', 'ease_in_out', 'ease_out', 'linear', 'step'})*

exactly `runtime.js`’s
`EASINGS` table (the stage engine draws these and no others), so the Python
spec of that rule refuses what the browser would refuse.

* **Type:**
  The easings the value-typed rule accepts

### an.timing.easing.apply_easing(spec, t, , names=None)

Apply an easing spec to a normalised parameter `t` in `[0, 1]`.

`names` restricts the string specs accepted to that collection — what an
engine that implements only part of the registry passes (the stage runtime
implements the legacy names; see `an.stage.easing`). Sequences
always take the legacy Bézier solver.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> apply_easing(None, 0.25)
0.25
>>> apply_easing("step", 0.99), apply_easing("step", 1.0)
(0.0, 1.0)
>>> apply_easing("smooth", 0.5, names={"linear"})
Traceback (most recent call last):
 ...
an.timing.easing.UnknownEasingError: unknown easing preset 'smooth'; known: ['linear']
```

### an.timing.easing.css_cubic_bezier(x1, y1, x2, y2)

The CSS `cubic-bezier(x1, y1, x2, y2)` curve, solved to well below 1e-9.

`x1` and `x2` must lie in `[0, 1]` (so the curve is a function of
time); `y` may overshoot.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`float`](https://docs.python.org/3/builtins/functions.html#float)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> css = css_cubic_bezier(0.42, 0.0, 0.58, 1.0)
>>> css(0.0), css(1.0), round(css(0.5), 12)
(0, 1, 0.5)
>>> css_cubic_bezier(1.5, 0, 0, 1)
Traceback (most recent call last):
 ...
ValueError: cubic-bezier x values must lie in [0, 1], got x1=1.5, x2=0
```

### an.timing.easing.css_steps(n, position='jump-end')

CSS `steps(n, position)`, following the CSS Easing Level 1 algorithm.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`float`](https://docs.python.org/3/builtins/functions.html#float)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> f = css_steps(4)
>>> [f(u) for u in (0.0, 0.24, 0.25, 0.99, 1.0)]
[0.0, 0.0, 0.25, 0.75, 1.0]
>>> css_steps(3, "jump-none")(0.5)
0.5
```

### an.timing.easing.easing_entries(, owner=None)

Every registered entry in registration order; only `owner`’s when given.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`EasingEntry`](_autosummary/an.timing.easing.html.md#an.timing.easing.EasingEntry), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.timing.easing.easing_entry(name)

The registered entry called `name`.

* **Return type:**
  [`EasingEntry`](_autosummary/an.timing.easing.html.md#an.timing.easing.EasingEntry)

```pycon
>>> easing_entry("ease").family
'legacy'
```

### an.timing.easing.legacy_cubic_bezier(cx1, cy1, cx2, cy2, t)

an’s cubic-Bézier easing at `t` — the `an-bezier-newton-8` solver.

The curve is defined by P0=(0,0), P1=(cx1,cy1), P2=(cx2,cy2), P3=(1,1).
Given a desired x=t we solve for the matching curve parameter u, then
return the y coordinate, by exactly 8 clamped Newton steps.

Structurally IDENTICAL to runtime.js::cubicBezier on purpose: always 8
iterations, break only on a degenerate derivative, clamp each step. This
function is the spec of that port, and the two are compared bit-for-bit by
the parity battery — an earlier version had an extra 

```
|u_new - u|
```

 < 1e-9
early-convergence break the JS side lacked, which left the two a ULP apart;
a numeric channel lerping large magnitudes amplifies a ULP of easing by
(b - a) (found by the an#86 adversarial review; the loops now match).

It does NOT agree with the CSS solver ([`css_cubic_bezier()`](_autosummary/an.timing.easing.html.md#an.timing.easing.css_cubic_bezier)) to 1e-9
for every control point, which is why it stays a named legacy solver until
the pixel goldens are re-blessed under the stricter one.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> round(legacy_cubic_bezier(0.0, 0.0, 1.0, 1.0, 0.5), 6)  # linear
0.5
>>> legacy_cubic_bezier(0.42, 0.0, 0.58, 1.0, 0.0), legacy_cubic_bezier(0.42, 0.0, 0.58, 1.0, 1.0)
(0.0, 1.0)
```

### an.timing.easing.register_easing(entry, , replace=False, owner=None)

Add `entry` to the registry (a genre’s own curves register here).

Re-registering a name is refused unless `replace=True`, and a replacement
must carry a HIGHER version: a name’s meaning changing under a scene that
uses it is exactly what entry versions exist to make visible, so it must be
deliberate and visible. `owner` names who registered it (core entries:
[`CORE_OWNER`](_autosummary/an.timing.easing.html.md#an.timing.easing.CORE_OWNER)); only core entries reach the contract files.

* **Return type:**
  [`EasingEntry`](_autosummary/an.timing.easing.html.md#an.timing.easing.EasingEntry)

### an.timing.easing.register_family(name, , owner=None)

Open a new easing family (refused if it exists).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.timing.easing.register_solver(name, description, , owner=None)

Name a new solver, with the description another language implements it from.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.timing.easing.resolve_easing(spec)

The curve an easing spec names.

- `None` -> linear;
- a registered name, `cubic-bezier(x1, y1, x2, y2)` or `steps(n[, position])`;
- a 4-sequence `[cx1, cy1, cx2, cy2]` -> the legacy Bézier solver.

Raises [`UnknownEasingError`](_autosummary/an.timing.easing.html.md#an.timing.easing.UnknownEasingError) (a `ValueError`) for an unknown name or
a malformed sequence, `TypeError` for any other type.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`float`](https://docs.python.org/3/builtins/functions.html#float)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.timing.easing.solvers(, owner=None)

The registered solvers; only `owner`’s when given.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/an.timing.flat.html.md

# an.timing.flat

The authored flat timeline: `(start, end, address, change)` rows.

The upper of the kernel’s two canonical levels (core study §2.4). Every authoring
front-end — `an`’s combinators, a keyframe sequence, Manim-style beats, recipes —
lowers to this list with absolute seconds, and a compiler lowers it to the
compiled form ([`an.timing.timeline`](_autosummary/an.timing.timeline.html.md#module-an.timing.timeline)). It is what `timeline.schema.json`
describes and what a package that copies `an`’s track format (`shaping`)
validates against.

A **change** is `set` (a value from `start` on) or `tween` (from an
optional `from` — absent means “from whatever the property has at `start`” —
to `to`, through an `easing`). Genre actions (a `play`, an `expression`)
are not changes: a genre lowers them before this level.

```pycon
>>> from types import SimpleNamespace as NS
>>> rows = [NS(start=0.0, end=1.0, action=NS(kind="tween", target="charlie/left_arm",
...     property="rotation", to_value=0.5, from_value=None, easing="ease_in_out"))]
>>> flat_timeline_doc(rows)["actions"]
[{'start': 0.0, 'end': 1.0, 'address': 'charlie/left_arm:rotation', 'change': {'kind': 'tween', 'to': 0.5, 'easing': 'ease_in_out'}}]
```

### Module Attributes

| [`FLAT_TIMELINE_FORMAT`](_autosummary/an.timing.flat.html.md#an.timing.flat.FLAT_TIMELINE_FORMAT)   | The document's self-description (`{kind, version}` envelope, core study §2.1).   |
|-------------------------------------------------------------------------|----------------------------------------------------------------------------------|

### Functions

| [`change_of`](_autosummary/an.timing.flat.html.md#an.timing.flat.change_of)(action, \*[, default_easing])    | The `change` of one leaf action (any object with the IR's attribute names).   |
|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`flat_timeline_doc`](_autosummary/an.timing.flat.html.md#an.timing.flat.flat_timeline_doc)(flat_actions, \*[, ...]) | A `timeline.schema.json` document from flat actions.                          |

### Exceptions

| [`UnsupportedChangeError`](_autosummary/an.timing.flat.html.md#an.timing.flat.UnsupportedChangeError)   | A flat action is not a `set` or a `tween` (a genre action not yet lowered).   |
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------|

### an.timing.flat.FLAT_TIMELINE_FORMAT *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an.timeline'*

The document’s self-description (`{kind, version}` envelope, core study §2.1).

### *exception* an.timing.flat.UnsupportedChangeError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A flat action is not a `set` or a `tween` (a genre action not yet lowered).

### an.timing.flat.change_of(action, , default_easing=None)

The `change` of one leaf action (any object with the IR’s attribute names).

A tween’s easing is written RESOLVED and always (`null` is linear): the
tween’s own, else `default_easing` (the scene’s `meta.default_easing`),
else the IR default — the precedence `TweenAction.resolved_easing` states,
so the flat document says what compiles (an#166).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.flat.flat_timeline_doc(flat_actions, , duration=None, default_easing=None, skip_other=False)

A `timeline.schema.json` document from flat actions.

Each item has `start`, `end` and a leaf `action` with `kind`,
`target`, `property` and the change’s values — the shape of
`an.ir.compose.flatten`’s output, read by attribute so this module does not
import the IR. `default_easing` is the scene’s `meta.default_easing`.
`skip_other` drops non-change actions instead of raising.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]


# _autosummary/an.timing.html.md

# an.timing

The timing kernel: what is on screen at time `t`, as a pure function.

The heart of the core (core study §3, layer K; ADR 0001 decisions 10-11). Every
genre that can answer “what is the value of this property at time t” does it the
same way: **addressed** properties ([`address`](_autosummary/an.timing.address.html.md#module-an.timing.address)), each of a
declared **field kind** ([`kinds`](_autosummary/an.timing.kinds.html.md#module-an.timing.kinds)) chosen by its entity kind’s
**property space** ([`spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)), keyed over time with **easing**
([`easing`](_autosummary/an.timing.easing.html.md#module-an.timing.easing)), flattened to absolute times
([`flat`](_autosummary/an.timing.flat.html.md#module-an.timing.flat)), compiled to tracks of placed clips of channels
([`channel`](_autosummary/an.timing.channel.html.md#module-an.timing.channel), [`clip`](_autosummary/an.timing.clip.html.md#module-an.timing.clip), [`timeline`](_autosummary/an.timing.timeline.html.md#module-an.timing.timeline))
and evaluated by `evaluate_timeline(timeline, t)` — sparse: a property nothing
has started writing is absent, and the entity’s rest state supplies it.

The kernel is a **cross-language contract** before it is code: five JSON files
in `an/data/timing/` generated from these registries ([`contract`](_autosummary/an.timing.contract.html.md#module-an.timing.contract)),
which a second implementation (TypeScript) asserts. `runtime.js` — the stage
engine’s evaluator — is a bit-exact port of this package’s value-typed rule, and
its parity tests hold it there.

The frame clock is `an`’s: frame `k` at `k / fps`, `frame_count = max(1,
round(duration * fps))` ([`an.frame_clock`](_autosummary/an.frame_clock.html.md#module-an.frame_clock)).

```pycon
>>> from an.timing import Channel, Keyframe, evaluate_channel, get_space
>>> ch = Channel("charlie", "x", [Keyframe(0.0, 0.0, "ease_in_out"), Keyframe(1.0, 10.0)])
>>> evaluate_channel(ch, 0.25, kind=get_space("stage.node").kind_of("x"))
1.25
```

### Functions

| [`format_address`](_autosummary/an.timing.html.md#an.timing.format_address)(target, prop)                       | The address string of a compiled `(target, property)` pair, unvalidated (the fast path for keying states; [`Address.of()`](_autosummary/an.timing.html.md#an.timing.Address.of) validates).   |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`parse_address`](_autosummary/an.timing.html.md#an.timing.parse_address)(text)                                | `text` as an [`Address`](_autosummary/an.timing.html.md#an.timing.Address); raises [`AddressError`](_autosummary/an.timing.html.md#an.timing.AddressError) naming the fault.                         |
| [`check_channel`](_autosummary/an.timing.html.md#an.timing.check_channel)(channel, kind)                       | Why `channel`'s keyframe values do not fit `kind` (empty if they do).                                                                                                                 |
| [`evaluate_channel`](_autosummary/an.timing.html.md#an.timing.evaluate_channel)(channel, t, \*[, kind])           | Evaluate `channel` at time `t` (see the module docstring for `kind`).                                                                                                                 |
| [`merge_poses`](_autosummary/an.timing.html.md#an.timing.merge_poses)(\*poses)                               | Merge multiple poses with **override semantics** (later wins per key).                                                                                                                |
| [`wrap_time`](_autosummary/an.timing.html.md#an.timing.wrap_time)(t, duration, loop_mode)                  | The public name of the loop rule (the contract's; `_wrap_time` is kept for the callers and tests that already import it).                                                             |
| [`evaluate_clip`](_autosummary/an.timing.html.md#an.timing.evaluate_clip)(clip, t, \*[, kind_of])              | Evaluate `clip` at time `t`, returning a `Pose`.                                                                                                                                      |
| [`apply_easing`](_autosummary/an.timing.html.md#an.timing.apply_easing)(spec, t, \*[, names])                 | Apply an easing spec to a normalised parameter `t` in `[0, 1]`.                                                                                                                       |
| [`easing_entries`](_autosummary/an.timing.html.md#an.timing.easing_entries)(\*[, owner])                        | Every registered entry in registration order; only `owner`'s when given.                                                                                                              |
| [`easing_entry`](_autosummary/an.timing.html.md#an.timing.easing_entry)(name)                                 | The registered entry called `name`.                                                                                                                                                   |
| [`register_easing`](_autosummary/an.timing.html.md#an.timing.register_easing)(entry, \*[, replace, owner])       | Add `entry` to the registry (a genre's own curves register here).                                                                                                                     |
| [`resolve_easing`](_autosummary/an.timing.html.md#an.timing.resolve_easing)(spec)                               | The curve an easing spec names.                                                                                                                                                       |
| [`kind_from_spec`](_autosummary/an.timing.html.md#an.timing.kind_from_spec)(spec)                               | A kind instance from its contract spec (`{"kind": name, **params}`), a bare kind name, or an instance (returned as is).                                                               |
| [`kind_names`](_autosummary/an.timing.html.md#an.timing.kind_names)(\*[, owner])                            | The registered kind names in registration order; only `owner`'s when given.                                                                                                           |
| [`register_kind`](_autosummary/an.timing.html.md#an.timing.register_kind)(name, factory, \*[, replace, owner]) | Register a field kind under `name` (how a genre adds one).                                                                                                                            |
| `get_space`(name)                                                                                   |                                                                                                                                                                                       |
| [`register_space`](_autosummary/an.timing.html.md#an.timing.register_space)(space, \*[, replace, owner])        | Register `space` under its name (how an entity kind declares its properties).                                                                                                         |
| [`space_from_json`](_autosummary/an.timing.html.md#an.timing.space_from_json)(doc)                               | A space from its JSON form (what `PropertySpace.to_json()` writes).                                                                                                                   |
| [`space_names`](_autosummary/an.timing.html.md#an.timing.space_names)(\*[, owner])                           | The registered space names in registration order; only `owner`'s when given.                                                                                                          |
| [`clip_from_json`](_autosummary/an.timing.html.md#an.timing.clip_from_json)(anim, \*[, name])                   | One compiled animation (`compiled.schema.json`'s `animation`) as a [`Clip`](_autosummary/an.timing.html.md#an.timing.Clip).                                                             |
| [`evaluate_timeline`](_autosummary/an.timing.html.md#an.timing.evaluate_timeline)(timeline, t, \*[, space])        | Evaluate `timeline` at time `t`, merging poses across tracks/clips.                                                                                                                   |
| [`timeline_from_compiled`](_autosummary/an.timing.html.md#an.timing.timeline_from_compiled)(doc)                        | The compiled document's `timeline`/`animations` as an evaluable `Timeline`.                                                                                                           |

### Classes

| [`Address`](_autosummary/an.timing.html.md#an.timing.Address)(entity[, nodes, field, qualifier])    | A parsed address.                                                                       |
|------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| [`Channel`](_autosummary/an.timing.html.md#an.timing.Channel)(target, property[, keyframes])        | Sorted keyframes for one property of one target.                                        |
| [`Keyframe`](_autosummary/an.timing.html.md#an.timing.Keyframe)(time, value[, easing])               | One keyframe: time, value, optional per-segment easing.                                 |
| [`Clip`](_autosummary/an.timing.html.md#an.timing.Clip)(name, duration[, channels, loop_mode])   | Named animation: a duration + a bundle of channels.                                     |
| [`LoopMode`](_autosummary/an.timing.html.md#an.timing.LoopMode)(\*values)                            | How a clip behaves past its natural duration.                                           |
| [`EasingEntry`](_autosummary/an.timing.html.md#an.timing.EasingEntry)(name, curve, family, solver, ...) | One named easing: its curve, where it comes from, and how it is solved.                 |
| [`AngleKind`](_autosummary/an.timing.html.md#an.timing.AngleKind)([unit, wrap])                       | An angle: the shortest arc when `wrap`, else the raw numbers.                           |
| [`ColorKind`](_autosummary/an.timing.html.md#an.timing.ColorKind)([space])                            | A colour (hex or `[r, g, b, a?]` in 0..1), written back in the form of the start value. |
| [`DiscreteKind`](_autosummary/an.timing.html.md#an.timing.DiscreteKind)([switch_at])                     | Holds `a` and switches to `b` on TIME (see the module docstring).                       |
| [`FieldKind`](_autosummary/an.timing.html.md#an.timing.FieldKind)()                                   | Base of every field kind: a frozen, hashable, JSON-describable interpolator.            |
| [`NumberKind`](_autosummary/an.timing.html.md#an.timing.NumberKind)([space])                           | A continuous scalar.                                                                    |
| [`OrbitKind`](_autosummary/an.timing.html.md#an.timing.OrbitKind)([unit])                             | A camera orbiting a target: `{azimuth, elevation, distance, target?}`.                  |
| [`QuaternionKind`](_autosummary/an.timing.html.md#an.timing.QuaternionKind)()                              | A unit quaternion `[x, y, z, w]`, interpolated by slerp after a sign fix.               |
| [`Segment`](_autosummary/an.timing.html.md#an.timing.Segment)(t, start, end)                        | Where an evaluation sits: time `t` inside the segment `[start, end)`.                   |
| [`VectorKind`](_autosummary/an.timing.html.md#an.timing.VectorKind)()                                  | A fixed-length list of numbers, interpolated componentwise.                             |
| [`FieldDecl`](_autosummary/an.timing.html.md#an.timing.FieldDecl)(pattern, kind[, unit, writes, ...]) | One declaration of a property space: pattern -> kind, write group, unit.                |
| [`PropertySpace`](_autosummary/an.timing.html.md#an.timing.PropertySpace)(name[, fields, version, ...])   | An entity kind's properties: declarations, matched exact-first then by glob.            |
| [`PlacedClip`](_autosummary/an.timing.html.md#an.timing.PlacedClip)(clip[, start_time, duration, ...]) | A clip placed at an absolute time on a track.                                           |
| [`Timeline`](_autosummary/an.timing.html.md#an.timing.Timeline)(duration[, tracks, space])           | A duration + ordered list of tracks.                                                    |
| [`Track`](_autosummary/an.timing.html.md#an.timing.Track)([target_root, clips])                   | A sequence of placed clips that share a common purpose / target prefix.                 |

### Exceptions

| [`AddressError`](_autosummary/an.timing.html.md#an.timing.AddressError)       | A string is not an address under the grammar.                               |
|---------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`UnknownEasingError`](_autosummary/an.timing.html.md#an.timing.UnknownEasingError) | An easing spec names no registered curve and parses as no parametrised one. |
| [`FieldKindError`](_autosummary/an.timing.html.md#an.timing.FieldKindError)     | A kind spec is malformed, or names no registered kind.                      |
| [`SpaceError`](_autosummary/an.timing.html.md#an.timing.SpaceError)         | A property space is malformed, unknown, or already registered.              |

### *class* an.timing.Address(entity, nodes=(), field='', qualifier=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A parsed address. `str(address)` writes it back unchanged.

#### *property* field_path *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

The dotted field, split.

#### *classmethod* of(target, prop)

The address of a compiled `(target, property)` pair (validated).

* **Return type:**
  [`Address`](_autosummary/an.timing.address.html.md#an.timing.address.Address)

#### *property* property *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

the field and its qualifier.

* **Type:**
  The compiled form’s `property`

#### *property* target *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

the entity and its node path.

* **Type:**
  The compiled form’s `target`

### *exception* an.timing.AddressError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A string is not an address under the grammar.

### *class* an.timing.AngleKind(unit='deg', wrap=True)

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

An angle: the shortest arc when `wrap`, else the raw numbers. Interpolated
angles are not re-normalised (350 -> 10 passes through 360).

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### *class* an.timing.Channel(target, property, keyframes=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Sorted keyframes for one property of one target.

Construction validates that `keyframes` is non-empty and sorted.

### *class* an.timing.Clip(name, duration, channels=<factory>, loop_mode=LoopMode.ONCE)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Named animation: a duration + a bundle of channels.

### *class* an.timing.ColorKind(space='oklab')

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A colour (hex or `[r, g, b, a?]` in 0..1), written back in the form of
the start value. `oklab` mixes perceptually with premultiplied alpha;
`srgb` lerps the encoded channels (Manim’s and the stage tint’s rule).

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### *class* an.timing.DiscreteKind(switch_at=0.5)

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

Holds `a` and switches to `b` on TIME (see the module docstring).

`switch_at` lies in `(0, 1]`. The default, 0.5, is the switch point of an
undeclared property; a replacement drawing (a swap set) declares 1, so it
never appears before its own key.

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

#### switches *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[bool](https://docs.python.org/3/builtins/functions.html#bool)]* *= True*

True when the value (or part of it) switches on TIME rather than moving.

### *class* an.timing.EasingEntry(name, curve, family, solver, description, version=1, params=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One named easing: its curve, where it comes from, and how it is solved.

`version` follows ADR 0003: an entry whose meaning changes gets a new
version, so a shot that names it re-renders visibly instead of silently.

#### to_json()

The entry as the contract file lists it (without samples).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *class* an.timing.FieldDecl(pattern, kind, unit=None, writes=None, description='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One declaration of a property space: pattern -> kind, write group, unit.

#### writes *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

The write group this property belongs to; `None` means itself.

### *class* an.timing.FieldKind

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Base of every field kind: a frozen, hashable, JSON-describable interpolator.

Subclasses set `name` and implement [`interpolate()`](_autosummary/an.timing.html.md#an.timing.FieldKind.interpolate); the dataclass
fields are the kind’s parameters, and `to_spec()` writes them as the
contract does (`{"kind": name, **non-default params}`).

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

#### switches *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[bool](https://docs.python.org/3/builtins/functions.html#bool)]* *= False*

True when the value (or part of it) switches on TIME rather than moving.

### *exception* an.timing.FieldKindError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A kind spec is malformed, or names no registered kind.

### *class* an.timing.Keyframe(time, value, easing=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One keyframe: time, value, optional per-segment easing.

The easing on a keyframe describes the curve **leaving** that keyframe
toward the next one. The last keyframe’s easing is therefore unused.

### *class* an.timing.LoopMode(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

How a clip behaves past its natural duration.

### *class* an.timing.NumberKind(space='linear')

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A continuous scalar. `space="log"` interpolates the logarithm (zoom, scale).

Linear interpolation is `a + (b - a) * u` with no special case at the ends,
which is the stage runtime’s arithmetic: a declared `number` channel is
bit-identical to what `runtime.js` draws.

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### *class* an.timing.OrbitKind(unit='deg')

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A camera orbiting a target: `{azimuth, elevation, distance, target?}`.

Azimuth takes the shortest arc, elevation is lerped and clamped to the poles,
distance is lerped in log, target componentwise. Any other member is discrete
and switches at `DEFAULT_SWITCH_AT` (on time). One value: a dotted
address never reaches inside it.

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

#### switches *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[bool](https://docs.python.org/3/builtins/functions.html#bool)]* *= True*

True when the value (or part of it) switches on TIME rather than moving.

### *class* an.timing.PlacedClip(clip, start_time=0.0, duration=None, speed=1.0, blend_in=0.0, blend_out=0.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A clip placed at an absolute time on a track.

#### *property* effective_duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Duration this clip occupies on the timeline (after speed scaling).

### *class* an.timing.PropertySpace(name, fields=(), version=1, description='', undeclared=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An entity kind’s properties: declarations, matched exact-first then by glob.

#### declaration(prop)

The declaration governing `prop`, or `None`.

* **Return type:**
  [`FieldDecl`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.FieldDecl) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### undeclared *: [FieldKind](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)*

The kind of a property no declaration matches.

### *class* an.timing.QuaternionKind

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A unit quaternion `[x, y, z, w]`, interpolated by slerp after a sign fix.

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### *class* an.timing.Segment(t, start, end)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where an evaluation sits: time `t` inside the segment `[start, end)`.

#### switched(switch_at)

Whether a discrete value has switched to the segment’s second key.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> Segment(0.5, 0.0, 1.0).switched(0.5), Segment(0.49, 0.0, 1.0).switched(0.5)
(True, False)
>>> Segment(0.9999999999, 0.0, 1.0).switched(1.0)  # only at b.time itself
False
```

### *exception* an.timing.SpaceError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A property space is malformed, unknown, or already registered.

### *class* an.timing.Timeline(duration, tracks=<factory>, space=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A duration + ordered list of tracks. The canonical playback structure.

`space` is the property space the timeline’s DOCUMENT declares for its
targets (an#245): one space, a registered name, or a `target -> space`
resolver. It is what [`evaluate_timeline()`](_autosummary/an.timing.html.md#an.timing.evaluate_timeline) uses when the caller passes
none, so every caller of the default evaluates a target in the space its
entity’s kind declares. `None` (every document that declares nothing) is
the kernel default, [`an.timing.spaces.DFLT_TIMELINE_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_TIMELINE_SPACE).

### *class* an.timing.Track(target_root='', clips=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A sequence of placed clips that share a common purpose / target prefix.

`target_root` is informational metadata for downstream tools (the JS
runtime can use it to scope rendering); evaluation does not filter by it.

### *exception* an.timing.UnknownEasingError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

An easing spec names no registered curve and parses as no parametrised one.

### *class* an.timing.VectorKind

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A fixed-length list of numbers, interpolated componentwise.

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.timing.apply_easing(spec, t, , names=None)

Apply an easing spec to a normalised parameter `t` in `[0, 1]`.

`names` restricts the string specs accepted to that collection — what an
engine that implements only part of the registry passes (the stage runtime
implements the legacy names; see `an.stage.easing`). Sequences
always take the legacy Bézier solver.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> apply_easing(None, 0.25)
0.25
>>> apply_easing("step", 0.99), apply_easing("step", 1.0)
(0.0, 1.0)
>>> apply_easing("smooth", 0.5, names={"linear"})
Traceback (most recent call last):
 ...
an.timing.easing.UnknownEasingError: unknown easing preset 'smooth'; known: ['linear']
```

### an.timing.check_channel(channel, kind)

Why `channel`’s keyframe values do not fit `kind` (empty if they do).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### an.timing.clip_from_json(anim, , name=None)

One compiled animation (`compiled.schema.json`’s `animation`) as a [`Clip`](_autosummary/an.timing.html.md#an.timing.Clip).

Accepts the JSON mapping or any object with the same attributes (the stage’s
`AnimationClipJSON`). Two fields are carried rather than defaulted, and
both have cost a bug: `loop_mode` (without it every loop evaluated as
`once` — an#7) and a list-valued `easing`, which is a cubic-bezier
control quadruple and must stay a tuple for `Keyframe`. The stage compiler
reads a from-less tween’s start through this too (an#212), so it evaluates
exactly what the runtime will.

* **Return type:**
  [`Clip`](_autosummary/an.timing.clip.html.md#an.timing.clip.Clip)

### an.timing.easing_entries(, owner=None)

Every registered entry in registration order; only `owner`’s when given.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`EasingEntry`](_autosummary/an.timing.easing.html.md#an.timing.easing.EasingEntry), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.timing.easing_entry(name)

The registered entry called `name`.

* **Return type:**
  [`EasingEntry`](_autosummary/an.timing.easing.html.md#an.timing.easing.EasingEntry)

```pycon
>>> easing_entry("ease").family
'legacy'
```

### an.timing.evaluate_channel(channel, t, , kind=None)

Evaluate `channel` at time `t` (see the module docstring for `kind`).

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.timing.evaluate_clip(clip, t, , kind_of=None)

Evaluate `clip` at time `t`, returning a `Pose`.

`kind_of` declares each channel’s field kind; `None` interpolates by
value type, as `runtime.js` does (see [`an.timing.channel`](_autosummary/an.timing.channel.html.md#module-an.timing.channel)).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.evaluate_timeline(timeline, t, , space=None)

Evaluate `timeline` at time `t`, merging poses across tracks/clips.

The result is a PURE function of `t` (an#185): what a node shows at `t`
never depends on which instants were evaluated before it. Per
`(target, property)`:

- **Active** — some clip writing it is playing at `t` (inclusive end:
  a clip at `[s, e]` is active at `t == e` too, so the final frame of
  “play this from 0 to 1 s” is visible at 1.0). Later wins: track order,
  then clip order within a track. Written at `t`.
- **Held** — no clip writing it is playing, but one has ended: the value
  the clip reached AT ITS END holds. The latest end wins; a tie goes to
  the later clip, the same “later wins” as above. Written at that end.
- **At rest** — nothing writing it has started yet. The key is ABSENT from
  the pose, and its value is the node’s own (the entity’s rest state;
  `runtime.js` restores what it built).

Keys that write the same thing on one node (a write group: the swap sets of
one visual, `rotation`/`rotation_rad`) keep only the most recently
WRITTEN — an ended `viseme@happy` span does not outlive the `viseme`
track that took the mouth back.

`space` says what each property is ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)): one space, a
registered space’s name, or a `target -> space` resolver. Its field kinds
interpolate and its write groups resolve. `None` is the default space,
[`an.timing.spaces.DFLT_TIMELINE_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_TIMELINE_SPACE) (the stage node’s declared
kinds, by name at call time). `space=VALUE_TYPED` is the rule
`runtime.js` implements — interpolation by value type, the `stage.node`
write groups — which gives the same pose on every timeline the compiler
emits (it refuses a value that fails its field kind).

Forward-order rendering used to show the value at the clip’s last SAMPLED
frame instead (the runtime kept whatever it last applied). The two agree
whenever a clip ends on the frame grid — true of every golden-corpus clip
— and differ when it ends between frames: a 0.37 s tween to 10 at 24 fps
used to stop at 9.80 and now lands on 10, as authored. That landing is
deliberate (it is the bug the motion presets’ settling `set` patched one
preset at a time), and it is what makes the pose independent of the grid.
Also deliberate: a clip shorter than a frame that no frame lands in now
leaves its end value, and a held descendant tint stays on top of an
ancestor’s later tint (the more specific target wins, as it always did
while both played).

`runtime.js::evaluateTimeline` is a port of this function and
`cutan`’s `tests/test_pure_pose.py` holds the two to it.

```pycon
>>> from an.timing.channel import Channel, Keyframe
>>> from an.timing.clip import Clip
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> tl = Timeline(2.0, [Track("a", [PlacedClip(Clip("m", 1.0, [ch]), 0.5)])])
>>> evaluate_timeline(tl, 0.0)  # not started: at rest, so absent
{}
>>> evaluate_timeline(tl, 1.0)[("a", "x")]  # active
5.0
>>> evaluate_timeline(tl, 1.75)[("a", "x")]  # ended: its end value holds
10.0
```

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.format_address(target, prop)

The address string of a compiled `(target, property)` pair, unvalidated
(the fast path for keying states; [`Address.of()`](_autosummary/an.timing.html.md#an.timing.Address.of) validates).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.timing.kind_from_spec(spec)

A kind instance from its contract spec (`{"kind": name, **params}`),
a bare kind name, or an instance (returned as is).

* **Return type:**
  [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

```pycon
>>> kind_from_spec("discrete")
DiscreteKind(switch_at=0.5)
>>> kind_from_spec({"kind": "points"})
Traceback (most recent call last):
 ...
an.timing.kinds.FieldKindError: unknown field kind 'points'; known: ['number', 'angle', 'vector', 'quaternion', 'color', 'orbit', 'discrete']
```

### an.timing.kind_names(, owner=None)

The registered kind names in registration order; only `owner`’s when given.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.timing.merge_poses(\*poses)

Merge multiple poses with **override semantics** (later wins per key).

Used by the timeline to combine concurrent clips on the same target.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> merge_poses({("a", "x"): 1.0}, {("a", "x"): 2.0, ("a", "y"): 3.0})
{('a', 'x'): 2.0, ('a', 'y'): 3.0}
```

### an.timing.parse_address(text)

`text` as an [`Address`](_autosummary/an.timing.html.md#an.timing.Address); raises [`AddressError`](_autosummary/an.timing.html.md#an.timing.AddressError) naming the fault.

* **Return type:**
  [`Address`](_autosummary/an.timing.address.html.md#an.timing.address.Address)

```pycon
>>> parse_address("charlie/left_arm:rotation").target
'charlie/left_arm'
>>> parse_address("a::b")
Traceback (most recent call last):
 ...
an.timing.address.AddressError: 'a::b': an address has exactly one ':', found 2
```

### an.timing.register_easing(entry, , replace=False, owner=None)

Add `entry` to the registry (a genre’s own curves register here).

Re-registering a name is refused unless `replace=True`, and a replacement
must carry a HIGHER version: a name’s meaning changing under a scene that
uses it is exactly what entry versions exist to make visible, so it must be
deliberate and visible. `owner` names who registered it (core entries:
`CORE_OWNER`); only core entries reach the contract files.

* **Return type:**
  [`EasingEntry`](_autosummary/an.timing.easing.html.md#an.timing.easing.EasingEntry)

### an.timing.register_kind(name, factory, , replace=False, owner=None)

Register a field kind under `name` (how a genre adds one).

`owner` names who registered it; only `CORE_OWNER`’s kinds reach
`an`’s contract files.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)]

```pycon
>>> sorted(kind_names())[:3]
['angle', 'color', 'discrete']
```

### an.timing.register_space(space, , replace=False, owner=None)

Register `space` under its name (how an entity kind declares its properties).

`owner` names who registered it; only `CORE_OWNER`’s spaces reach
`an`’s contract files, so an installed genre never edits the core contract.

* **Return type:**
  [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)

### an.timing.resolve_easing(spec)

The curve an easing spec names.

- `None` -> linear;
- a registered name, `cubic-bezier(x1, y1, x2, y2)` or `steps(n[, position])`;
- a 4-sequence `[cx1, cy1, cx2, cy2]` -> the legacy Bézier solver.

Raises [`UnknownEasingError`](_autosummary/an.timing.html.md#an.timing.UnknownEasingError) (a `ValueError`) for an unknown name or
a malformed sequence, `TypeError` for any other type.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`float`](https://docs.python.org/3/builtins/functions.html#float)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.timing.space_from_json(doc)

A space from its JSON form (what `PropertySpace.to_json()` writes).

* **Return type:**
  [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)

### an.timing.space_names(, owner=None)

The registered space names in registration order; only `owner`’s when given.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.timing.timeline_from_compiled(doc)

The compiled document’s `timeline`/`animations` as an evaluable `Timeline`.

`doc` is the JSON mapping of `compiled.schema.json` or any object with
the same attributes (the stage’s `CutoutSceneJSON`). This is the Python
side of the parity contract: `evaluate_timeline` over what it returns is
the executable spec `runtime.js` is tested against.

`target_root` and the blend ramps are carried although nothing reads them
yet: a reader that quietly drops a field it was handed is a lossy “rebuilds
the evaluable form”.

* **Return type:**
  [`Timeline`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.Timeline)

```pycon
>>> doc = {"timeline": {"duration": 1.0, "tracks": [{"clips": [
...     {"animation_id": "m", "start_time": 0.0}]}]},
...     "animations": {"m": {"duration": 1.0, "channels": [{"target": "a",
...     "property": "x", "keyframes": [{"time": 0.0, "value": 0.0},
...     {"time": 1.0, "value": 4.0}]}]}}}
>>> evaluate_timeline(timeline_from_compiled(doc), 0.25)
{('a', 'x'): 1.0}
```

### an.timing.wrap_time(t, duration, loop_mode)

Apply the loop mode to `t`, returning a time within `[0, duration]`.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### Modules

| [`address`](_autosummary/an.timing.address.html.md#module-an.timing.address)   | The address grammar: one way to name an animatable value, in every genre.                    |
|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| [`channel`](_autosummary/an.timing.channel.html.md#module-an.timing.channel)   | Channel: keyframes for a single (target, property) pair, evaluated at time t.                |
| [`clip`](_autosummary/an.timing.clip.html.md#module-an.timing.clip)         | Clip: a named bundle of channels with a duration and loop mode.                              |
| [`contract`](_autosummary/an.timing.contract.html.md#module-an.timing.contract) | The timing kernel's cross-language contract: five JSON files, generated from the registries. |
| [`easing`](_autosummary/an.timing.easing.html.md#module-an.timing.easing)     | The easing registry: every named timing curve, each with the solver that computes it.        |
| [`flat`](_autosummary/an.timing.flat.html.md#module-an.timing.flat)         | The authored flat timeline: `(start, end, address, change)` rows.                            |
| [`kinds`](_autosummary/an.timing.kinds.html.md#module-an.timing.kinds)       | Field kinds: the declared interpolator of each animatable property.                          |
| [`spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)     | Property spaces: what an entity kind's properties ARE, declared once.                        |
| [`timeline`](_autosummary/an.timing.timeline.html.md#module-an.timing.timeline) | Timeline: tracks of placed clips with absolute times — the compiled evaluation form.         |


# _autosummary/an.timing.kinds.html.md

# an.timing.kinds

Field kinds: the declared interpolator of each animatable property.

**The declared kind picks the interpolator; a runtime value never does.** A field
kind is what lets a number be an angle (shortest arc), a log-space zoom, or a
colour mixed in OKLab — none of which a value’s type can say. Kinds are declared
per property by a *property space* ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)); a property no space
declares is [`DiscreteKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.DiscreteKind).

Seven kinds are seeded, each parametrised (the TypeScript kernel’s seven):

**Discrete is defined on TIME, never on a ratio or an eased value** (an#86). A
segment from key `a` to key `b` shows `b` iff `t >= a.time + switch_at *
(b.time - a.time)`, and `switch_at == 1` is evaluated as `t >= b.time` with
no arithmetic at all: `(t - a.time) / span` can round up to 1.0 while
`t < b.time`, and an overshooting easing would flip an eased comparison twice.

Genres add kinds with [`register_kind()`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.register_kind) (a Manim genre’s `points`, say)
without editing this module.

```pycon
>>> NumberKind().interpolate(0.0, 10.0, 0.25, Segment(0.25, 0.0, 1.0))
2.5
>>> NumberKind(space="log").interpolate(1.0, 4.0, 0.5, Segment(0.5, 0.0, 1.0))
2.0
>>> AngleKind().interpolate(350, 10, 0.5, Segment(0.5, 0.0, 1.0))  # not re-normalised
360.0
>>> b = 9.767899248713501
>>> DiscreteKind(switch_at=1).interpolate("A", "B", 1.0, Segment(math.nextafter(b, 0), 0.15, b))
'A'
>>> kind_from_spec({"kind": "number", "space": "log"})
NumberKind(space='log')
```

### Module Attributes

| [`DEFAULT_SWITCH_AT`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.DEFAULT_SWITCH_AT)         | Where an undeclared discrete value switches, in normalised segment time.                                          |
|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| [`QUATERNION_UNIT_TOLERANCE`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.QUATERNION_UNIT_TOLERANCE) | How far a quaternion's norm may be from 1 and still count as unit length.                                         |
| [`SLERP_LINEAR_THRESHOLD`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.SLERP_LINEAR_THRESHOLD)    | Above this <br/><br/>```<br/>|dot|<br/>```<br/><br/>, slerp falls back to normalised lerp (the arc is too short). |
| [`KindFactory`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.KindFactory)               | its parameters as keyword arguments -> an instance.                                                               |
| [`CORE_OWNER`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.CORE_OWNER)                | Who registered the seeded kinds (only these reach the contract files).                                            |

### Functions

| [`kind_from_spec`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.kind_from_spec)(spec)                               | A kind instance from its contract spec (`{"kind": name, **params}`), a bare kind name, or an instance (returned as is).   |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| [`kind_names`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.kind_names)(\*[, owner])                            | The registered kind names in registration order; only `owner`'s when given.                                               |
| [`register_kind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.register_kind)(name, factory, \*[, replace, owner]) | Register a field kind under `name` (how a genre adds one).                                                                |
| [`shortest_delta`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.shortest_delta)(a, b[, period])                     | The signed shortest turn from `a` to `b`, in `[-period/2, period/2]`.                                                     |
| [`slerp`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.slerp)(a, b, u)                                     | Spherical interpolation of unit quaternions `[x, y, z, w]`, the short way.                                                |

### Classes

| [`AngleKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.AngleKind)([unit, wrap])   | An angle: the shortest arc when `wrap`, else the raw numbers.                           |
|----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| [`ColorKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.ColorKind)([space])        | A colour (hex or `[r, g, b, a?]` in 0..1), written back in the form of the start value. |
| [`DiscreteKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.DiscreteKind)([switch_at]) | Holds `a` and switches to `b` on TIME (see the module docstring).                       |
| [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)()               | Base of every field kind: a frozen, hashable, JSON-describable interpolator.            |
| [`NumberKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.NumberKind)([space])       | A continuous scalar.                                                                    |
| [`OrbitKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.OrbitKind)([unit])         | A camera orbiting a target: `{azimuth, elevation, distance, target?}`.                  |
| [`QuaternionKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.QuaternionKind)()          | A unit quaternion `[x, y, z, w]`, interpolated by slerp after a sign fix.               |
| [`Segment`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.Segment)(t, start, end)    | Where an evaluation sits: time `t` inside the segment `[start, end)`.                   |
| [`VectorKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.VectorKind)()              | A fixed-length list of numbers, interpolated componentwise.                             |

### Exceptions

| [`FieldKindError`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKindError)   | A kind spec is malformed, or names no registered kind.   |
|-------------------------------------------------------------------|----------------------------------------------------------|

### *class* an.timing.kinds.AngleKind(unit='deg', wrap=True)

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

An angle: the shortest arc when `wrap`, else the raw numbers. Interpolated
angles are not re-normalised (350 -> 10 passes through 360).

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.timing.kinds.CORE_OWNER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an'*

Who registered the seeded kinds (only these reach the contract files).

### *class* an.timing.kinds.ColorKind(space='oklab')

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A colour (hex or `[r, g, b, a?]` in 0..1), written back in the form of
the start value. `oklab` mixes perceptually with premultiplied alpha;
`srgb` lerps the encoded channels (Manim’s and the stage tint’s rule).

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.timing.kinds.DEFAULT_SWITCH_AT *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.5*

Where an undeclared discrete value switches, in normalised segment time.

### *class* an.timing.kinds.DiscreteKind(switch_at=0.5)

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

Holds `a` and switches to `b` on TIME (see the module docstring).

`switch_at` lies in `(0, 1]`. The default, 0.5, is the switch point of an
undeclared property; a replacement drawing (a swap set) declares 1, so it
never appears before its own key.

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

#### switches *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[bool](https://docs.python.org/3/builtins/functions.html#bool)]* *= True*

True when the value (or part of it) switches on TIME rather than moving.

### *class* an.timing.kinds.FieldKind

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Base of every field kind: a frozen, hashable, JSON-describable interpolator.

Subclasses set `name` and implement [`interpolate()`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind.interpolate); the dataclass
fields are the kind’s parameters, and `to_spec()` writes them as the
contract does (`{"kind": name, **non-default params}`).

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

#### switches *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[bool](https://docs.python.org/3/builtins/functions.html#bool)]* *= False*

True when the value (or part of it) switches on TIME rather than moving.

### *exception* an.timing.kinds.FieldKindError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A kind spec is malformed, or names no registered kind.

### an.timing.kinds.KindFactory

its parameters as keyword arguments -> an instance.

* **Type:**
  A kind’s factory

alias of `Callable`[[…], [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)]

### *class* an.timing.kinds.NumberKind(space='linear')

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A continuous scalar. `space="log"` interpolates the logarithm (zoom, scale).

Linear interpolation is `a + (b - a) * u` with no special case at the ends,
which is the stage runtime’s arithmetic: a declared `number` channel is
bit-identical to what `runtime.js` draws.

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### *class* an.timing.kinds.OrbitKind(unit='deg')

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A camera orbiting a target: `{azimuth, elevation, distance, target?}`.

Azimuth takes the shortest arc, elevation is lerped and clamped to the poles,
distance is lerped in log, target componentwise. Any other member is discrete
and switches at [`DEFAULT_SWITCH_AT`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.DEFAULT_SWITCH_AT) (on time). One value: a dotted
address never reaches inside it.

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

#### switches *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[[bool](https://docs.python.org/3/builtins/functions.html#bool)]* *= True*

True when the value (or part of it) switches on TIME rather than moving.

### an.timing.kinds.QUATERNION_UNIT_TOLERANCE *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.001*

How far a quaternion’s norm may be from 1 and still count as unit length.

### *class* an.timing.kinds.QuaternionKind

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A unit quaternion `[x, y, z, w]`, interpolated by slerp after a sign fix.

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.timing.kinds.SLERP_LINEAR_THRESHOLD *: [float](https://docs.python.org/3/builtins/functions.html#float)* *= 0.9995*

Above this 

```
|dot|
```

, slerp falls back to normalised lerp (the arc is too short).

### *class* an.timing.kinds.Segment(t, start, end)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where an evaluation sits: time `t` inside the segment `[start, end)`.

#### switched(switch_at)

Whether a discrete value has switched to the segment’s second key.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> Segment(0.5, 0.0, 1.0).switched(0.5), Segment(0.49, 0.0, 1.0).switched(0.5)
(True, False)
>>> Segment(0.9999999999, 0.0, 1.0).switched(1.0)  # only at b.time itself
False
```

### *class* an.timing.kinds.VectorKind

Bases: [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

A fixed-length list of numbers, interpolated componentwise.

#### check(value)

Why `value` is not valid for this kind, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### interpolate(a, b, u, seg)

The value between keys `a` and `b` at eased progress `u`.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### an.timing.kinds.kind_from_spec(spec)

A kind instance from its contract spec (`{"kind": name, **params}`),
a bare kind name, or an instance (returned as is).

* **Return type:**
  [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)

```pycon
>>> kind_from_spec("discrete")
DiscreteKind(switch_at=0.5)
>>> kind_from_spec({"kind": "points"})
Traceback (most recent call last):
 ...
an.timing.kinds.FieldKindError: unknown field kind 'points'; known: ['number', 'angle', 'vector', 'quaternion', 'color', 'orbit', 'discrete']
```

### an.timing.kinds.kind_names(, owner=None)

The registered kind names in registration order; only `owner`’s when given.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.timing.kinds.register_kind(name, factory, , replace=False, owner=None)

Register a field kind under `name` (how a genre adds one).

`owner` names who registered it; only [`CORE_OWNER`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.CORE_OWNER)’s kinds reach
`an`’s contract files.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`FieldKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)]

```pycon
>>> sorted(kind_names())[:3]
['angle', 'color', 'discrete']
```

### an.timing.kinds.shortest_delta(a, b, period=360.0)

The signed shortest turn from `a` to `b`, in `[-period/2, period/2]`.

An exact half turn goes in the direction of `b - a`, so a return trip
retraces its path (0 -> 180 -> 0 goes there and back, not round).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> shortest_delta(350, 10), shortest_delta(10, 350), shortest_delta(0, 180)
(20.0, -20.0, 180.0)
```

### an.timing.kinds.slerp(a, b, u)

Spherical interpolation of unit quaternions `[x, y, z, w]`, the short way.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]


# _autosummary/an.timing.spaces.html.md

# an.timing.spaces

Property spaces: what an entity kind’s properties ARE, declared once.

A **property space** is the registration unit an entity kind hands the kernel
(ADR 0001 decision 11): for each property pattern, its **field kind** (the
interpolator, [`an.timing.kinds`](_autosummary/an.timing.kinds.html.md#module-an.timing.kinds)), its **write group** (aliases that set the
same thing — `rotation_rad` is `rotation`; every swap set of one visual
replaces the same drawing), and its **unit** (stage pixels, radians, a ratio),
without which moving a value from one engine to another is meaningless.

Patterns are exact property names or `fnmatch` globs (`*@*`). An exact name
wins; globs are tried in declaration order. A property the space does not declare
is [`DiscreteKind`](_autosummary/an.timing.kinds.html.md#an.timing.kinds.DiscreteKind) at the default switch point, and writes
only itself.

Two spaces are seeded here, and they reproduce today’s output EXACTLY:

- `stage.node` — a node of the 2D stage engine: every name in
  `an.base.TRANSFORM_PROPERTIES` is `number` (linear), including
  > `tint_r/g/b` (`tint` itself stays the compiler’s authoring sugar, expanded
  > into those three before any channel exists, so the wire shape does not move);
  > every other property names a swap set, `*@*` included, and is
  > `discrete(switch_at=1)`.
- `stage.camera` — the stage’s 2D framing camera: `x`, `y`, `zoom`,
  `rotation`, all plain `number` (the compiler lowers them onto `root`’s
  pivot, scale and rotation channels). New genres get log zoom and shortest-arc
  rotation by declaring their own camera space, not by changing this one.

Genres register their own spaces with [`register_space()`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.register_space) (P2: through the
`an.genres` entry point), without editing this module.

```pycon
>>> node = get_space("stage.node")
>>> node.kind_of("x"), node.kind_of("viseme@happy")
(NumberKind(space='linear'), DiscreteKind(switch_at=1))
>>> node.write_group("rotation_rad"), node.write_group("hands"), node.unit_of("rotation")
('rotation', '<swap>', 'rad')
>>> PropertySpace("demo", ()).kind_of("anything")  # undeclared: discrete
DiscreteKind(switch_at=0.5)
```

### Module Attributes

| [`SWAP_WRITE_GROUP`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.SWAP_WRITE_GROUP)       | they all replace the one drawing the node carries (`viseme` and `viseme@happy` both set the mouth's texture, an#88).                                                                                                                                                          |
|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`SWAP_KIND`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.SWAP_KIND)              | A replacement drawing never appears before its own key (an#86).                                                                                                                                                                                                               |
| [`STAGE_NODE_UNITS`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.STAGE_NODE_UNITS)       | What the stage node's transform properties are measured in.                                                                                                                                                                                                                   |
| [`STAGE_NODE_ALIASES`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.STAGE_NODE_ALIASES)     | Properties that write another property's value on the same node.                                                                                                                                                                                                              |
| [`CORE_OWNER`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.CORE_OWNER)             | Who registered the seeded spaces (only these reach the contract files).                                                                                                                                                                                                       |
| [`DFLT_VALUE_TYPED_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_VALUE_TYPED_SPACE) | The space whose write groups the value-typed rule ([`VALUE_TYPED`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.VALUE_TYPED)) uses: the stage engine's node, resolved BY NAME at call time, so moving the stage's registration (P3: into `an.stage`) or replacing it needs no kernel edit. |
| [`DFLT_TIMELINE_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_TIMELINE_SPACE)    | DECLARED field kinds from this registered space, resolved by name at call time (ADR 0001 decision 11: field kinds are declared, never inferred).                                                                                                                              |
| [`VALUE_TYPED`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.VALUE_TYPED)            | Pass as `space=` to ask for the value-typed rule `runtime.js` implements (interpolate by the value's runtime type, the [`DFLT_VALUE_TYPED_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_VALUE_TYPED_SPACE) write groups) — what the contract's parity lane holds the stage runtime to.    |
| [`SpaceLike`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.SpaceLike)              | one space for every target, a registered space's name, or a per-target resolver (P2: target -> its entity kind's space).                                                                                                                                                      |
| [`STAGE_NODE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.STAGE_NODE)             | P3 moves their registration into `an.stage`.                                                                                                                                                                                                                                  |

### Functions

| `get_space`(name)                                                                            |                                                                               |
|----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`register_space`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.register_space)(space, \*[, replace, owner]) | Register `space` under its name (how an entity kind declares its properties). |
| [`space_from_json`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.space_from_json)(doc)                        | A space from its JSON form (what `PropertySpace.to_json()` writes).           |
| [`space_names`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.space_names)(\*[, owner])                    | The registered space names in registration order; only `owner`'s when given.  |
| [`space_resolver`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.space_resolver)(space)                       | `space` as a `target -> PropertySpace` function.                              |

### Classes

| [`FieldDecl`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.FieldDecl)(pattern, kind[, unit, writes, ...])   | One declaration of a property space: pattern -> kind, write group, unit.     |
|--------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)(name[, fields, version, ...])     | An entity kind's properties: declarations, matched exact-first then by glob. |

### Exceptions

| [`SpaceError`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.SpaceError)   | A property space is malformed, unknown, or already registered.   |
|---------------------------------------------------------------|------------------------------------------------------------------|

### an.timing.spaces.CORE_OWNER *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'an'*

Who registered the seeded spaces (only these reach the contract files).

### an.timing.spaces.DFLT_TIMELINE_SPACE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= 'stage.node'*

DECLARED field
kinds from this registered space, resolved by name at call time (ADR 0001
decision 11: field kinds are declared, never inferred). The flip from the
value-typed rule happened in P2 (an#239 item 2), once the compiler refused
every keyframe value that fails its field kind — so the two rules agree on
everything the compiler emits (the corpus-frame test and the golden vectors,
which hold both). Every caller — compile’s from-less start, `impacts`’
truth projections, the bench — inherits it. `None` here would restore the
value-typed rule as the default.

ONE space for every target, by design for now: a compiled stage document’s
nodes are all stage nodes and it does not carry its entities’ kinds. The
per-entity policy ([`an.genres.entity_space_resolver()`](_autosummary/an.genres.html.md#an.genres.entity_space_resolver)) is what
`an validate` and the compiler’s keyframe check use; making it the
evaluator’s default too belongs to the `Engine` seam (P3, an#245), where
an engine knows its document’s entities (review-244 S7).

* **Type:**
  What `evaluate_timeline(..., space=None)` evaluates with

### an.timing.spaces.DFLT_VALUE_TYPED_SPACE *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'stage.node'*

The space whose write groups the value-typed rule ([`VALUE_TYPED`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.VALUE_TYPED)) uses:
the stage engine’s node, resolved BY NAME at call time, so moving the stage’s
registration (P3: into `an.stage`) or replacing it needs no kernel edit.

### *class* an.timing.spaces.FieldDecl(pattern, kind, unit=None, writes=None, description='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One declaration of a property space: pattern -> kind, write group, unit.

#### writes *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

The write group this property belongs to; `None` means itself.

### *class* an.timing.spaces.PropertySpace(name, fields=(), version=1, description='', undeclared=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An entity kind’s properties: declarations, matched exact-first then by glob.

#### declaration(prop)

The declaration governing `prop`, or `None`.

* **Return type:**
  [`FieldDecl`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.FieldDecl) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### undeclared *: [FieldKind](_autosummary/an.timing.kinds.html.md#an.timing.kinds.FieldKind)*

The kind of a property no declaration matches.

### an.timing.spaces.STAGE_NODE *: [PropertySpace](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)* *= PropertySpace(name='stage.node', fields=(FieldDecl(pattern='alpha', kind=NumberKind(space='linear'), unit='fraction', writes=None, description=''), FieldDecl(pattern='dash_offset', kind=NumberKind(space='linear'), unit='px', writes=None, description=''), FieldDecl(pattern='perspective', kind=NumberKind(space='linear'), unit='frame_height', writes=None, description=''), FieldDecl(pattern='pivot_x', kind=NumberKind(space='linear'), unit='px', writes=None, description=''), FieldDecl(pattern='pivot_y', kind=NumberKind(space='linear'), unit='px', writes=None, description=''), FieldDecl(pattern='plane_fade_end', kind=NumberKind(space='linear'), unit='px', writes=None, description=''), FieldDecl(pattern='plane_fade_start', kind=NumberKind(space='linear'), unit='px', writes=None, description=''), FieldDecl(pattern='rotation', kind=NumberKind(space='linear'), unit='rad', writes=None, description=''), FieldDecl(pattern='rotation_rad', kind=NumberKind(space='linear'), unit='rad', writes='rotation', description=''), FieldDecl(pattern='rotation_x', kind=NumberKind(space='linear'), unit='rad', writes=None, description=''), FieldDecl(pattern='scale_x', kind=NumberKind(space='linear'), unit='ratio', writes=None, description=''), FieldDecl(pattern='scale_y', kind=NumberKind(space='linear'), unit='ratio', writes=None, description=''), FieldDecl(pattern='skew_x', kind=NumberKind(space='linear'), unit='rad', writes=None, description=''), FieldDecl(pattern='skew_y', kind=NumberKind(space='linear'), unit='rad', writes=None, description=''), FieldDecl(pattern='tint_b', kind=NumberKind(space='linear'), unit='fraction', writes=None, description=''), FieldDecl(pattern='tint_g', kind=NumberKind(space='linear'), unit='fraction', writes=None, description=''), FieldDecl(pattern='tint_r', kind=NumberKind(space='linear'), unit='fraction', writes=None, description=''), FieldDecl(pattern='trim_end', kind=NumberKind(space='linear'), unit='fraction', writes=None, description=''), FieldDecl(pattern='trim_start', kind=NumberKind(space='linear'), unit='fraction', writes=None, description=''), FieldDecl(pattern='x', kind=NumberKind(space='linear'), unit='px', writes=None, description=''), FieldDecl(pattern='y', kind=NumberKind(space='linear'), unit='px', writes=None, description=''), FieldDecl(pattern='\*@\*', kind=DiscreteKind(switch_at=1), unit=None, writes='<swap>', description='a variant swap set (viseme@happy): replacement animation'), FieldDecl(pattern='\*', kind=DiscreteKind(switch_at=1), unit=None, writes='<swap>', description='any other property names a swap set: replacement animation')), version=1, description='a node of the 2D stage engine: transform properties interpolate as plain numbers; every other property is a swap set that switches at its own key', undeclared=DiscreteKind(switch_at=0.5))*

P3 moves their registration
into `an.stage`. Kernel code resolves them by name (`get_space`).

* **Type:**
  Provisional handles on the seeded stage spaces

### an.timing.spaces.STAGE_NODE_ALIASES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'rotation_rad': 'rotation'}*

Properties that write another property’s value on the same node.

### an.timing.spaces.STAGE_NODE_UNITS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'alpha': 'fraction', 'dash_offset': 'px', 'perspective': 'frame_height', 'pivot_x': 'px', 'pivot_y': 'px', 'plane_fade_end': 'px', 'plane_fade_start': 'px', 'rotation': 'rad', 'rotation_rad': 'rad', 'rotation_x': 'rad', 'scale_x': 'ratio', 'scale_y': 'ratio', 'skew_x': 'rad', 'skew_y': 'rad', 'tint_b': 'fraction', 'tint_g': 'fraction', 'tint_r': 'fraction', 'trim_end': 'fraction', 'trim_start': 'fraction', 'x': 'px', 'y': 'px'}*

What the stage node’s transform properties are measured in.

### an.timing.spaces.SWAP_KIND *: [DiscreteKind](_autosummary/an.timing.kinds.html.md#an.timing.kinds.DiscreteKind)* *= DiscreteKind(switch_at=1)*

A replacement drawing never appears before its own key (an#86).

### an.timing.spaces.SWAP_WRITE_GROUP *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '<swap>'*

they all replace the
one drawing the node carries (`viseme` and `viseme@happy` both set the
mouth’s texture, an#88).

* **Type:**
  The write group every swap set on a stage node shares

### *exception* an.timing.spaces.SpaceError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A property space is malformed, unknown, or already registered.

### an.timing.spaces.SpaceLike

one space for every target, a
registered space’s name, or a per-target resolver (P2: target -> its entity
kind’s space).

* **Type:**
  What an evaluator accepts as “the space”

alias of [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | `Callable`[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)]

### an.timing.spaces.VALUE_TYPED *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'value-typed'*

Pass as `space=` to ask for the value-typed rule `runtime.js` implements
(interpolate by the value’s runtime type, the [`DFLT_VALUE_TYPED_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_VALUE_TYPED_SPACE)
write groups) — what the contract’s parity lane holds the stage runtime to.

### an.timing.spaces.register_space(space, , replace=False, owner=None)

Register `space` under its name (how an entity kind declares its properties).

`owner` names who registered it; only [`CORE_OWNER`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.CORE_OWNER)’s spaces reach
`an`’s contract files, so an installed genre never edits the core contract.

* **Return type:**
  [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)

### an.timing.spaces.space_from_json(doc)

A space from its JSON form (what `PropertySpace.to_json()` writes).

* **Return type:**
  [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)

### an.timing.spaces.space_names(, owner=None)

The registered space names in registration order; only `owner`’s when given.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### an.timing.spaces.space_resolver(space)

`space` as a `target -> PropertySpace` function.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`PropertySpace`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.PropertySpace)]


# _autosummary/an.timing.timeline.html.md

# an.timing.timeline

Timeline: tracks of placed clips with absolute times — the compiled evaluation form.

A `Timeline` is a flat description of *what plays when*: tracks of
[`PlacedClip`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.PlacedClip), each a [`Clip`](_autosummary/an.timing.clip.html.md#an.timing.clip.Clip) at an absolute start,
with a duration override, a speed and recorded blend ramps. It is the level the
stage runtime evaluates (`runtime.js::evaluateTimeline` is a port of
[`evaluate_timeline()`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.evaluate_timeline)) and the level the contract’s golden vectors exercise
(`compiled.schema.json`). Authoring composition trees (`an.ir.compose`) are
flattened into it by a compiler.

`blend_in` and `blend_out` ramps are recorded but **not applied** to pose
values: the timeline produces the raw pose.

```pycon
>>> from an.timing.channel import Channel, Keyframe
>>> from an.timing.clip import Clip
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> clip = Clip("walk", duration=1.0, channels=[ch])
>>> tl = Timeline(duration=2.0, tracks=[Track("a", clips=[PlacedClip(clip, start_time=0.5)])])
>>> evaluate_timeline(tl, 1.0)[("a", "x")]
5.0
```

### Functions

| [`write_group`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.write_group)(prop)                           | What `prop` writes on a STAGE node (the `stage.node` space's groups).       |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`evaluate_timeline`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.evaluate_timeline)(timeline, t, \*[, space]) | Evaluate `timeline` at time `t`, merging poses across tracks/clips.         |
| [`clip_from_json`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.clip_from_json)(anim, \*[, name])            | One compiled animation (`compiled.schema.json`'s `animation`) as a `Clip`.  |
| [`timeline_from_compiled`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.timeline_from_compiled)(doc)                 | The compiled document's `timeline`/`animations` as an evaluable `Timeline`. |

### Classes

| [`PlacedClip`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.PlacedClip)(clip[, start_time, duration, ...])   | A clip placed at an absolute time on a track.                           |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`Track`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.Track)([target_root, clips])                     | A sequence of placed clips that share a common purpose / target prefix. |
| [`Timeline`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.Timeline)(duration[, tracks, space])             | A duration + ordered list of tracks.                                    |

### *class* an.timing.timeline.PlacedClip(clip, start_time=0.0, duration=None, speed=1.0, blend_in=0.0, blend_out=0.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A clip placed at an absolute time on a track.

#### *property* effective_duration *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Duration this clip occupies on the timeline (after speed scaling).

### *class* an.timing.timeline.Timeline(duration, tracks=<factory>, space=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A duration + ordered list of tracks. The canonical playback structure.

`space` is the property space the timeline’s DOCUMENT declares for its
targets (an#245): one space, a registered name, or a `target -> space`
resolver. It is what [`evaluate_timeline()`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.evaluate_timeline) uses when the caller passes
none, so every caller of the default evaluates a target in the space its
entity’s kind declares. `None` (every document that declares nothing) is
the kernel default, [`an.timing.spaces.DFLT_TIMELINE_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_TIMELINE_SPACE).

### *class* an.timing.timeline.Track(target_root='', clips=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A sequence of placed clips that share a common purpose / target prefix.

`target_root` is informational metadata for downstream tools (the JS
runtime can use it to scope rendering); evaluation does not filter by it.

### an.timing.timeline.clip_from_json(anim, , name=None)

One compiled animation (`compiled.schema.json`’s `animation`) as a `Clip`.

Accepts the JSON mapping or any object with the same attributes (the stage’s
`AnimationClipJSON`). Two fields are carried rather than defaulted, and
both have cost a bug: `loop_mode` (without it every loop evaluated as
`once` — an#7) and a list-valued `easing`, which is a cubic-bezier
control quadruple and must stay a tuple for `Keyframe`. The stage compiler
reads a from-less tween’s start through this too (an#212), so it evaluates
exactly what the runtime will.

* **Return type:**
  [`Clip`](_autosummary/an.timing.clip.html.md#an.timing.clip.Clip)

### an.timing.timeline.evaluate_timeline(timeline, t, , space=None)

Evaluate `timeline` at time `t`, merging poses across tracks/clips.

The result is a PURE function of `t` (an#185): what a node shows at `t`
never depends on which instants were evaluated before it. Per
`(target, property)`:

- **Active** — some clip writing it is playing at `t` (inclusive end:
  a clip at `[s, e]` is active at `t == e` too, so the final frame of
  “play this from 0 to 1 s” is visible at 1.0). Later wins: track order,
  then clip order within a track. Written at `t`.
- **Held** — no clip writing it is playing, but one has ended: the value
  the clip reached AT ITS END holds. The latest end wins; a tie goes to
  the later clip, the same “later wins” as above. Written at that end.
- **At rest** — nothing writing it has started yet. The key is ABSENT from
  the pose, and its value is the node’s own (the entity’s rest state;
  `runtime.js` restores what it built).

Keys that write the same thing on one node (a write group: the swap sets of
one visual, `rotation`/`rotation_rad`) keep only the most recently
WRITTEN — an ended `viseme@happy` span does not outlive the `viseme`
track that took the mouth back.

`space` says what each property is ([`an.timing.spaces`](_autosummary/an.timing.spaces.html.md#module-an.timing.spaces)): one space, a
registered space’s name, or a `target -> space` resolver. Its field kinds
interpolate and its write groups resolve. `None` is the default space,
[`an.timing.spaces.DFLT_TIMELINE_SPACE`](_autosummary/an.timing.spaces.html.md#an.timing.spaces.DFLT_TIMELINE_SPACE) (the stage node’s declared
kinds, by name at call time). `space=VALUE_TYPED` is the rule
`runtime.js` implements — interpolation by value type, the `stage.node`
write groups — which gives the same pose on every timeline the compiler
emits (it refuses a value that fails its field kind).

Forward-order rendering used to show the value at the clip’s last SAMPLED
frame instead (the runtime kept whatever it last applied). The two agree
whenever a clip ends on the frame grid — true of every golden-corpus clip
— and differ when it ends between frames: a 0.37 s tween to 10 at 24 fps
used to stop at 9.80 and now lands on 10, as authored. That landing is
deliberate (it is the bug the motion presets’ settling `set` patched one
preset at a time), and it is what makes the pose independent of the grid.
Also deliberate: a clip shorter than a frame that no frame lands in now
leaves its end value, and a held descendant tint stays on top of an
ancestor’s later tint (the more specific target wins, as it always did
while both played).

`runtime.js::evaluateTimeline` is a port of this function and
`cutan`’s `tests/test_pure_pose.py` holds the two to it.

```pycon
>>> from an.timing.channel import Channel, Keyframe
>>> from an.timing.clip import Clip
>>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)])
>>> tl = Timeline(2.0, [Track("a", [PlacedClip(Clip("m", 1.0, [ch]), 0.5)])])
>>> evaluate_timeline(tl, 0.0)  # not started: at rest, so absent
{}
>>> evaluate_timeline(tl, 1.0)[("a", "x")]  # active
5.0
>>> evaluate_timeline(tl, 1.75)[("a", "x")]  # ended: its end value holds
10.0
```

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### an.timing.timeline.timeline_from_compiled(doc)

The compiled document’s `timeline`/`animations` as an evaluable `Timeline`.

`doc` is the JSON mapping of `compiled.schema.json` or any object with
the same attributes (the stage’s `CutoutSceneJSON`). This is the Python
side of the parity contract: `evaluate_timeline` over what it returns is
the executable spec `runtime.js` is tested against.

`target_root` and the blend ramps are carried although nothing reads them
yet: a reader that quietly drops a field it was handed is a lossy “rebuilds
the evaluable form”.

* **Return type:**
  [`Timeline`](_autosummary/an.timing.timeline.html.md#an.timing.timeline.Timeline)

```pycon
>>> doc = {"timeline": {"duration": 1.0, "tracks": [{"clips": [
...     {"animation_id": "m", "start_time": 0.0}]}]},
...     "animations": {"m": {"duration": 1.0, "channels": [{"target": "a",
...     "property": "x", "keyframes": [{"time": 0.0, "value": 0.0},
...     {"time": 1.0, "value": 4.0}]}]}}}
>>> evaluate_timeline(timeline_from_compiled(doc), 0.25)
{('a', 'x'): 1.0}
```

### an.timing.timeline.write_group(prop)

What `prop` writes on a STAGE node (the `stage.node` space’s groups).

Two keys in one group set the same thing, so only the more recently written
can be showing: every swap set on a node swaps the one visual it carries
(`viseme` and `viseme@happy` both set the mouth’s texture, an#88), and
`rotation_rad` is `rotation`. Every other runtime property
(`an.base.TRANSFORM_PROPERTIES`, the runtime’s own switch) writes only
itself.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> write_group("x"), write_group("rotation_rad"), write_group("viseme@happy")
('x', 'rotation', '<swap>')
```


# _autosummary/an.tools.html.md

# an.tools

User-facing utility functions, plus the SSOT list for CLI dispatch.

Each function here is meant to be callable from Python *and* from the shell
via `an <funcname>`. Keep their signatures dispatch-friendly: positional args
become required arguments, defaults become optional flags, and the docstring
becomes the command’s help. `an/__main__.py` projects this list onto typer
without touching these functions, so they stay plain Python.

### Functions

| [`bench`](_autosummary/an.tools.html.md#an.tools.bench)([scenes, out, keep_render, quiet, ...])     | Render the fixed bench corpus and write a metrics ledger.                             |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| [`bench_compare`](_autosummary/an.tools.html.md#an.tools.bench_compare)([before, after, mutation, ...])     | Compare two ledger rows — and refuse when they are not comparable.                    |
| [`bench_mutants`](_autosummary/an.tools.html.md#an.tools.bench_mutants)([names, quiet])                     | Break each guard on purpose and check the test that names it goes red.                |
| [`check`](_autosummary/an.tools.html.md#an.tools.check)()                                           | Print a status report of all backend system + Python deps.                            |
| [`credits`](_autosummary/an.tools.html.md#an.tools.credits)(project_dir[, json_out])                  | Show what third-party work is in `project_dir` and what it obliges.                   |
| [`init`](_autosummary/an.tools.html.md#an.tools.init)(project_dir[, name, force, id, genre, ...])  | Create a fresh an project at `project_dir` — or, with --id, at the default location.  |
| [`iterate`](_autosummary/an.tools.html.md#an.tools.iterate)(project_dir, instruction[, ...])          | Apply a free-text instruction to the scene.                                           |
| [`preview`](_autosummary/an.tools.html.md#an.tools.preview)(project_dir[, shot, no_browser])          | Live-preview the project's scene in a browser; reloads on edit.                       |
| [`probe`](_autosummary/an.tools.html.md#an.tools.probe)(project_dir[, shot, at, out, columns, ...]) | Write a shot's frame at chosen instants to a PNG, drawn the way `an render` draws it. |
| [`registered_namespaces`](_autosummary/an.tools.html.md#an.tools.registered_namespaces)()                           | The sub-namespaces installed genres added (`cli.<namespace>` services).               |
| [`render`](_autosummary/an.tools.html.md#an.tools.render)(project_dir[, output_name, tts, ...])      | Render the project at `project_dir` to a single mp4.                                  |
| [`sync`](_autosummary/an.tools.html.md#an.tools.sync)(project_dir[, accept_measured])              | Reconcile scene.md and ir/scene.json inside `project_dir`.                            |
| [`validate`](_autosummary/an.tools.html.md#an.tools.validate)(project_dir[, fps, strict_assets])       | Validate the scene at `project_dir`.                                                  |

### an.tools.bench(scenes='', out='', keep_render='', quiet=False, bless='', compare='', mutation='')

Render the fixed bench corpus and write a metrics ledger.

The instrument, not the verdict: it records numbers whose predicted
direction under each deliberate degradation is declared in advance, so a
future regression is caught by something other than someone noticing.

scenes: comma-separated corpus scene names (default: all of them)
out: ledger path (default: misc/bench/ledger/<date>-<sha>[-dirty].json)
keep_render: keep the throwaway render tree here instead of deleting it
quiet: print only the ledger path
bless: (re)write the golden frames, recording THIS STRING as the reason
compare: after the run, compare it against this baseline ledger row
mutation: pull one declared lever for this run, and ask –compare the

> per-mutation question instead of “is the second row worse”

Rendering knobs are deliberately NOT flags: a bench whose render knobs vary
per invocation produces incomparable rows, so they are a module constant
recorded verbatim into the ledger. `--mutation` is not one of them, and is
the exception that states the rule: a lever is the **independent variable**,
it is named in the report, it is exempted by declaration in
`MUTATION_TOUCHES` rather than by widening anything, and the row it
produces is never filed under a commit’s name. Without it the `--compare`
artifact is always the `mutation=None` path, which asks “is this worse” of
a run that was broken on purpose — the wrong question, answered
confidently.

`--bless` takes the reason as its value rather than pairing with a
separate `--reason`, so a bless with no recorded reason cannot be typed.
A re-bless with no recorded reason is the same failure as a silently
widened threshold, and it is the failure this wave exists to prevent — so
look at the PNG diff (GitHub renders 2-up, swipe and onion-skin) before
writing one.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.bench_compare(before='', after='', mutation='', strict=False, raw=False)

Compare two ledger rows — and refuse when they are not comparable.

before: baseline ledger row (default: the second-newest committed row)
after: the row to judge (default: the newest committed row)
mutation: evaluate the per-mutation predictions instead of asking whether

> the second row is worse. One of the mutations the rows declare.

strict: exit nonzero when the answer is bad — a regression without a
: mutation, or a direction that reverses on its own metric’s threshold
  grid (an#140: some cell of it got worse), an unmet criterion with one,
  a comparison that answered nothing, or a row that could not be read at
  all. For CI.

raw: print the report as JSON instead of the human digest

Refusing is the feature. Two rows measured on different scenes, at
different resolutions, or on different x264 builds are not “one better and
one worse” — every number in them is uninterpretable relative to the other,
and a number reported across incomparable rows is worse than none.

`-dirty` rows are excluded from the defaults: a row measured against
uncommitted edits describes no commit. Name one explicitly to compare it.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.bench_mutants(names='', quiet=False)

Break each guard on purpose and check the test that names it goes red.

names: comma-separated mutant names (default: every declared one)
quiet: print only the tally

“N mutants, all caught” is unfalsifiable after the fact when the mutations
lived in a scratch script. These are declared data, so the proof is
re-runnable — which matters because Wave 1 shipped three guards that stayed
green while the bug they guarded was present.

The WHOLE guard file runs for each mutant, never a `-k` filter: a filter that
happens to exclude the catching test reports “not caught” and sends you to
write a test that already exists. Takes about forty seconds.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.check()

Print a status report of all backend system + Python deps.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.credits(project_dir, json_out=None)

Show what third-party work is in `project_dir` and what it obliges.

project_dir: the an project
json_out: also write the machine-readable record to this path

A licence recorded and never displayed is not compliance, so this is the
consumer that makes the provenance field worth having.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.init(project_dir, name=None, force=False, id=False, genre='', package='', root='')

Create a fresh an project at `project_dir` — or, with –id, at the default location.

project_dir: where to create the project (created if missing); with –id, the project’s id instead (an init <id> –id; –id is a switch and takes no value)
name: project display name (defaults to the directory name)
force: overwrite an existing scene.md
id: a switch: read the positional as a project id, and create the project under its genre’s projects folder (an init alice-and-bob –id –genre cutout_animation -> ~/.local/share/cutan/projects/alice-and-bob)
genre: with –id, the genre the video is made in (e.g. cutout_animation); the genre names the package whose root holds the project
package: with –id, that package directly, overriding –genre (default: the genre’s, else an)
root: with –id, that package’s root (default: its data folder, or <PKG>_HOME)

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.iterate(project_dir, instruction, apply_changes=True, model='claude-opus-4-7')

Apply a free-text instruction to the scene. Needs ANTHROPIC_API_KEY.

project_dir: path to an an project
instruction: what to change in plain English (e.g. “make Maya’s laugh longer and warmer”)
apply_changes: persist the new scene to disk (default True); the next render

> re-renders exactly the shots whose content changed

model: Anthropic model id (default claude-opus-4-7)

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.preview(project_dir, shot='', no_browser=False)

Live-preview the project’s scene in a browser; reloads on edit.

Spins up a local HTTP server pointed at the runtime canvas. The
browser polls `scene.json` for changes and re-loads when you save
`scene.md`. Lossy: visuals only, no audio. Blocks until Ctrl-C.

project_dir: path to an an project (must contain scene.md / ir/scene.json)
shot: shot id to preview (default: first shot in the timeline)
no_browser: don’t auto-open the default browser

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.probe(project_dir, shot='', at=None, out='', columns=0, supersample=1, allow_private_here=False)

Write a shot’s frame at chosen instants to a PNG, drawn the way `an render` draws it.

The shot is prepared as a render prepares it (captions, style pack, the
settled length) and its own renderer draws only the frames asked for; a
Manim shot is read from the picture the last render stored. Several –at
make one grid. Frames showing private or unknown material are refused at
a path git would pick up.

project_dir: path to an an project
shot: the shot id
at: seconds into the shot, repeatable (–at 1.5 –at 3.8)
out: the PNG to write (default: <project>/artifacts/probes/<shot>@<t>.png, gitignored)
columns: cells per row of a grid (default: a square grid)
supersample: as `an render --supersample`, so the frame matches the film’s
allow_private_here: write not-publishable frames where git would pick them up

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.registered_namespaces()

The sub-namespaces installed genres added (`cli.<namespace>` services).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)]

```pycon
>>> registered_namespaces() == {} or all(isinstance(v, list) for v in registered_namespaces().values())
True
```

### an.tools.render(project_dir, output_name='main', tts='', lipsync='offline', parallel='', strict_assets=False, supersample=1, pix_fmt='', step_hz=0.0, language='en', capture='', force_render=False, no_cache=False, cache_frames=False)

Render the project at `project_dir` to a single mp4.

Incremental: a shot whose inputs (its compiled document, its art’s bytes,
its audio, the runtime, the render settings, this machine’s browser and
ffmpeg) are unchanged since a previous render is reused from the shot cache
rather than rendered again, and the summary line says which were which.

Then what the render learned, grouped, each with its fix: a synthesized line
that runs past its shot or into a dissolve, a stand-in or a substitution,
a library pin out of date (artifacts/render_reports/<output_name>.json
holds them all).

project_dir: path to an an project (must contain scene.md / ir/scene.json)
output_name: filename stem under output/ (default: “main”)
tts: TTS provider for EVERY line — “offline” (silent), “elevenlabs” (needs

> ELEVEN_API_KEY) or “mac_say”. Omitted (the default): each voice’s own
> `provider` speaks its lines, a voice naming none is offline, and what a
> paid provider will bill is printed before the first request. A line
> spoken by another provider than its voice declares is a finding (an
> error under –strict-assets)

lipsync: lip-sync provider — “offline” (deterministic), “rhubarb”
: (needs the rhubarb binary), or “whisper” (needs faster-whisper)

parallel: per-shot concurrency. “” or “1” = serial (default); “auto” =
: min(shots, cpu, 4); a number ≥ 2 caps the thread pool.

strict_assets: fail instead of drawing a stand-in (the placeholder rig, the
: default backdrop) for an asset the project’s stores don’t supply

supersample: render at N times the resolution and resolve back with an exact
: N x N block mean. Opt-in; 1 (the default) costs nothing at all. Measured
  on the shipped path at 1920x1080: 125.5 ms/frame at 1, 508.6 at 2 —
  4.05x, which is NOT the 2.54x the research reports for the render alone

pix_fmt: the delivered encode’s pixel format — “yuv420p” (default) or
: “yuv444p”. The ONE first-order quality lever in the encoder: 4:4:4 cuts
  the edge-band error 11.35 -> 3.79, where a mathematically lossless 4:2:0
  only reaches 10.15. It is opt-in for a PRODUCT reason and not an encoder
  one: High 4:4:4 Predictive is refused by many hardware decoders,
  browsers and platforms, so a 4:4:4 file is one some viewers cannot play

step_hz: stepped timing for authored tweens — pose updates per second, on a
: shot-wide grid (15 at 30 fps is “on twos”, 10 “on threes”). 0 (the
  default) uses the scene’s own `meta.step_hz`, which is unset (smooth)
  unless the author declared one. The camera, blinks, `play` clips and
  swap channels are never stepped by this

language: the dialogue’s language (BCP-47) for providers that select
: behaviour by it — Rhubarb’s recognizer today: English (the default)
  uses `pocketSphinx` with the transcript, anything else `phonetic`
  without one (an#96)

capture: how frames leave the browser — “canvas” (the default), an in-page
: read of the canvas in batches, ~7.8x faster frame stage on the corpus,
  ~2.3x at 1080p; or “screenshot”, a Playwright element screenshot per
  instant. Both write frames with the same decoded pixels

force_render: render every shot even when the shot cache holds it (and
: refresh its entry)

no_cache: neither read nor write the shot cache — every shot is rendered
: cold, as before the cache existed

cache_frames: no longer needed, and no effect on `an render` (an#260): a
: film with transitions or a sound layer now reuses its shots by default,
  caching only the frames at each transition. Kept so scripts that pass
  it still run

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.sync(project_dir, accept_measured=False)

Reconcile scene.md and ir/scene.json inside `project_dir`.

`--accept-measured` also writes the durations a clock-owning renderer
(Manim) MEASURED into the scene — each such shot’s `duration:` line,
patched in place, so the prose around it is kept. Without it, a measured
duration lives only in the derived `measurements` store and the scene
says what its author wrote.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.tools.validate(project_dir, fps=0.0, strict_assets=False)

Validate the scene at `project_dir`. Prints findings, exit 0 on pass.

project_dir: the an project
fps: the frame rate `an render --fps` will use, when not the scene’s (0: the scene’s)
strict_assets: judge the scene as `an render --strict-assets` will: a stand-in for a missing asset is an error

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.util.html.md

# an.util

Internal helpers: file I/O, hashing, time arithmetic, light path utilities.

Nothing here is part of the public API — everything is underscore-prefixed
or used only by sibling an modules.


# _autosummary/an.verify.html.md

# an.verify

Verification protocol — same interface for human, lint, vision-LM, MoVer.

`StyleLintVerifier` is imported on first use, not here: `an.verify.style` is
also a script (`python -m an.verify.style`), and a package that imports the
module it is about to run as `__main__` makes `runpy` warn that it is
already in `sys.modules`.

### Classes

| [`Verifier`](_autosummary/an.verify.html.md#an.verify.Verifier)(\*args, \*\*kwargs)                    | Pluggable verifier.                            |
|--------------------------------------------------------------------------------------------------|------------------------------------------------|
| [`Finding`](_autosummary/an.verify.html.md#an.verify.Finding)(severity, ir_path, description[, ...])  | A single verification issue.                   |
| [`VerificationReport`](_autosummary/an.verify.html.md#an.verify.VerificationReport)([passed, findings])          | Result of running one or more verifiers.       |
| [`LayoutLintVerifier`](_autosummary/an.verify.html.md#an.verify.LayoutLintVerifier)()                            | Cheap IR-only verifier.                        |
| [`HumanInTheLoopVerifier`](_autosummary/an.verify.html.md#an.verify.HumanInTheLoopVerifier)(\*[, prompt])            | Open the mp4, prompt the user to approve.      |
| [`MediaQualityVerifier`](_autosummary/an.verify.html.md#an.verify.MediaQualityVerifier)(\*[, max_db_floor, ...])   | Post-render quality checks.                    |
| [`VisionLMVerifier`](_autosummary/an.verify.html.md#an.verify.VisionLMVerifier)(\*[, model, frame_count, ...]) | Claude vision Verifier (skip-if-missing-deps). |

### *class* an.verify.Finding(severity, ir_path, description, suggested_fix=None, location=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single verification issue.

`ir_path` lets the orchestrator route the fix to the correct layer of
the IR — e.g. `"timeline/0/dialogue/1"`. `location` is set when the
thing to fix is not IR at all but an OPAQUE SOURCE the shot runs — a Manim
scene file — as `"<file>:<line>"` (core study §2.10): the fix then goes
to that line, and `ir_path` still names the shot that runs it.

```pycon
>>> Finding("warning", "timeline/0/options/source", "text cut off",
...         location="assets/sources/chart.py:14").location
'assets/sources/chart.py:14'
```

### *class* an.verify.HumanInTheLoopVerifier(, prompt='Approve render? [y/N/r=reject]: ')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Open the mp4, prompt the user to approve. Implements `Verifier`.

### *class* an.verify.LayoutLintVerifier

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Cheap IR-only verifier. Implements `Verifier`.

### *class* an.verify.MediaQualityVerifier(, max_db_floor=-75.0, dialogue_silence_ratio=0.7, frozen_ssim_threshold=0.999, frame_sample_fps=4.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Post-render quality checks. Implements `Verifier`.

### *class* an.verify.VerificationReport(passed=True, findings=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of running one or more verifiers.

### *class* an.verify.Verifier(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Pluggable verifier. Same interface for human, lint, vision-LM, MoVer.

#### verify(ir, render)

Verify a scene + (optional) render result. `render` may be None
for pre-render lint passes.

* **Return type:**
  [`VerificationReport`](_autosummary/an.verify.html.md#an.verify.VerificationReport)

### *class* an.verify.VisionLMVerifier(, model='claude-haiku-4-5-20251001', frame_count=4, max_tokens=800, api_key=None, judge=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Claude vision Verifier (skip-if-missing-deps).

#### judge

Injected, per “no globals, no service locators” — and because
injection is what makes record-vs-replay drift impossible: the
recorded and replayed paths are the same call through the same
object, differing only in what the store returns.

### Modules

| [`human`](_autosummary/an.verify.human.html.md#module-an.verify.human)                 | HumanInTheLoopVerifier — opens the rendered mp4 and asks for approval.                       |
|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| [`layout`](_autosummary/an.verify.layout.html.md#module-an.verify.layout)               | LayoutLintVerifier — cheap pre-render checks on the IR.                                      |
| [`media`](_autosummary/an.verify.media.html.md#module-an.verify.media)                 | Media verification helpers — audio + frame quality checks for rendered mp4s.                 |
| [`media_quality`](_autosummary/an.verify.media_quality.html.md#module-an.verify.media_quality) | MediaQualityVerifier — post-render quality checks on the actual mp4.                         |
| [`prosody`](_autosummary/an.verify.prosody.html.md#module-an.verify.prosody)             | Prosody measurement: how a recorded voice delivers its words, as numbers a target can check. |
| [`vision`](_autosummary/an.verify.vision.html.md#module-an.verify.vision)               | VisionLMVerifier — Claude vision looks at sampled frames and reports issues.                 |


# _autosummary/an.verify.human.html.md

# an.verify.human

HumanInTheLoopVerifier — opens the rendered mp4 and asks for approval.

Only useful in interactive sessions. Skips silently when there’s no TTY (CI,
agent contexts). The orchestrator can detect the skip via the report’s
informational finding.

### Classes

| [`HumanInTheLoopVerifier`](_autosummary/an.verify.human.html.md#an.verify.human.HumanInTheLoopVerifier)(\*[, prompt])   | Open the mp4, prompt the user to approve.   |
|-----------------------------------------------------------------------------------------|---------------------------------------------|

### *class* an.verify.human.HumanInTheLoopVerifier(, prompt='Approve render? [y/N/r=reject]: ')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Open the mp4, prompt the user to approve. Implements `Verifier`.


# _autosummary/an.verify.layout.html.md

# an.verify.layout

LayoutLintVerifier — cheap pre-render checks on the IR.

Runs the same semantic checks as `an.ir.validate.validate_semantic` plus a
few that need a render context (do dialogue lines fit in their shot’s
duration? is the scene’s total duration consistent with the timeline?).

Deliberately runs on the IR alone (RenderResult=None is fine), so the
orchestrator can call it BEFORE rendering and skip a costly render if the
IR is broken.

### Classes

| [`LayoutLintVerifier`](_autosummary/an.verify.layout.html.md#an.verify.layout.LayoutLintVerifier)()   | Cheap IR-only verifier.   |
|-------------------------------------------------------------------------|---------------------------|

### *class* an.verify.layout.LayoutLintVerifier

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Cheap IR-only verifier. Implements `Verifier`.


# _autosummary/an.verify.media.html.md

# an.verify.media

Media verification helpers — audio + frame quality checks for rendered mp4s.

Phase 8 Tier 2. These complement the existing IR-only `LayoutLintVerifier`
by inspecting actual mp4 output: silence detection (catches cutoff dialogue),
audio level (catches missing audio), and per-frame perceptual diff via SSIM
(catches “all frames are identical” or “scene drifted between frames”).

Designed to depend only on `ffmpeg` / `ffprobe` and `numpy` (Pillow when
loading frames). No scikit-image, no opencv.

### Functions

| [`audio_volume`](_autosummary/an.verify.media.html.md#an.verify.media.audio_volume)(media_path)                        | Return dict with mean_db and max_db of the media's audio stream.               |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`detect_silence`](_autosummary/an.verify.media.html.md#an.verify.media.detect_silence)(media_path, \*[, noise_db, ...]) | Return `SilenceSpan``s in the audio of ``media_path` via ffmpeg.               |
| [`extract_frames`](_autosummary/an.verify.media.html.md#an.verify.media.extract_frames)(media_path, out_dir, \*[, ...])  | Extract frames from `media_path` at `fps` to `out_dir`.                        |
| [`ssim`](_autosummary/an.verify.media.html.md#an.verify.media.ssim)(a, b)                                      | A global-moment structural-similarity score between two single-channel images. |
| [`ssim_image_files`](_autosummary/an.verify.media.html.md#an.verify.media.ssim_image_files)(path_a, path_b)                | SSIM between two image files (any format Pillow can read).                     |
| [`transcribe`](_autosummary/an.verify.media.html.md#an.verify.media.transcribe)(media_path, \*[, model_size])        | Return the transcribed speech in `media_path`.                                 |

### Classes

| [`SilenceSpan`](_autosummary/an.verify.media.html.md#an.verify.media.SilenceSpan)(start, end)   | A contiguous run of near-silence inside an audio stream.   |
|----------------------------------------------------------------------------|------------------------------------------------------------|

### *class* an.verify.media.SilenceSpan(start, end)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A contiguous run of near-silence inside an audio stream.

### an.verify.media.audio_volume(media_path)

Return dict with mean_db and max_db of the media’s audio stream.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### an.verify.media.detect_silence(media_path, , noise_db=-30.0, min_duration_s=0.3)

Return `SilenceSpan``s in the audio of ``media_path` via ffmpeg.

Wraps `ffmpeg -af silencedetect=...` and parses the stderr “silence_start”
/ “silence_end” lines. Useful for catching dialogue that got cut off
(silence at the start/end of a shot when speech was expected).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`SilenceSpan`](_autosummary/an.verify.media.html.md#an.verify.media.SilenceSpan)]

### an.verify.media.extract_frames(media_path, out_dir, , fps=4.0, pattern='frame_%04d.png')

Extract frames from `media_path` at `fps` to `out_dir`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### an.verify.media.ssim(a, b)

A global-moment structural-similarity score between two single-channel images.

**Not Wang et al.’s SSIM**, despite what this docstring said for a long
time: that estimator is computed over sliding local windows, and this one
uses one global mean, variance and covariance per image. The formula is the
same; the reduction is not, and the difference is the whole behaviour.
Global moments are blind to a small local change — a total eye-blink scores
0.9989 here — because the flat fill that is most of a cutout frame drags
the mean to 1.0.

That blindness is *fine for what this function is used for*: catching a
frozen render, where every pixel is identical or none is. It is not fine as
a quality metric, which is why [`an.bench.metrics.ssim_map()`](_autosummary/an.bench.metrics.html.md#an.bench.metrics.ssim_map) exists
beside it rather than replacing it — `MediaQualityVerifier`’s frozen-render
threshold was tuned against this reduction, and that verifier is in the
default orchestrate chain.

Inputs are float arrays in [0, 1]. Returns a float in roughly `[-1, 1]`;
1 means identical.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> import numpy as np
>>> x = np.zeros((8, 8), dtype=np.float32)
>>> ssim(x, x)
1.0
```

### an.verify.media.ssim_image_files(path_a, path_b)

SSIM between two image files (any format Pillow can read).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### an.verify.media.transcribe(media_path, , model_size='tiny')

Return the transcribed speech in `media_path`.

Lazily imports faster-whisper. Raises `RuntimeError` with a clear
message when the package isn’t installed.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/an.verify.media_quality.html.md

# an.verify.media_quality

MediaQualityVerifier — post-render quality checks on the actual mp4.

Phase 9. Implements the `Verifier` Protocol using helpers from
`an.verify.media`. Runs only when given a `RenderResult` (skips silently
on pre-render calls). Adds three signals to the orchestrator’s verify pass:

1. **Audio level**: max dB above a floor — catches “AAC stream present but
   silent because the TTS produced empty bytes”.
2. **Silence vs. dialogue**: if the IR contains dialogue, the rendered
   audio shouldn’t be near-silent for most of the duration.
3. **Frame motion**: the mean SSIM between adjacent sampled frames
   shouldn’t be ~1.0 — a frozen render reads as flat.

Each check produces a `Finding` whose severity is “warning” so the run
proceeds; the orchestrator can decide whether to surface or block.

### Classes

| [`MediaQualityVerifier`](_autosummary/an.verify.media_quality.html.md#an.verify.media_quality.MediaQualityVerifier)(\*[, max_db_floor, ...])   | Post-render quality checks.   |
|--------------------------------------------------------------------------------------------------|-------------------------------|

### *class* an.verify.media_quality.MediaQualityVerifier(, max_db_floor=-75.0, dialogue_silence_ratio=0.7, frozen_ssim_threshold=0.999, frame_sample_fps=4.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Post-render quality checks. Implements `Verifier`.


# _autosummary/an.verify.prosody.html.md

# an.verify.prosody

Prosody measurement: how a recorded voice delivers its words, as numbers a target can check.

“Read it like a deadpan narrator” is only checkable if the delivery is a set of
numbers. This module measures one spoken clip — a synthesized dialogue line, a
narration take, a reference recording studied privately — and compares the
measurement to `[low, high]` targets, the way `an.verify.style` does for
a render’s cadence. It knows nothing about characters, faces or genres: any
narrated video (cut-out, data-viz, math-viz) has a voice to measure.

What is measured ([`METRICS`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.METRICS) is the vocabulary a target may use):

- **Rate.** `articulation_rate_sps` — syllables per second of *speaking* time
  (pauses excluded), and `speech_rate_sps` — syllables per second of the whole
  clip from first to last sound. Syllables come from the `text` when given
  ([`count_syllables()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.count_syllables), an English vowel-group estimate); without a text both
  rates are `nan`.
- **Pauses.** A pause is a run of non-speech frames of at least
  `min_pause_s` between two stretches of speech (the clip’s own leading and
  trailing silence never count). `pause_share` is pause time over the clip’s
  span; `pauses_per_min`, `pause_median_s` and `pause_p90_s` describe the
  pauses themselves. A frame is speech when its level is within
  `speech_floor_db` of the clip’s 95th-percentile level.
- **Pitch.** A YIN fundamental-frequency track (de Cheveigné & Kawahara 2002)
  on speech frames. Movement is reported in \*\*semitones about the clip’s own
  median\*\*, so a target transfers between a low and a high voice:
  `f0_sd_st` (spread), `f0_range_st` (5th to 95th percentile), and
  `final_drop_st` — per phrase (speech between pauses of at least
  `phrase_pause_s`), the median pitch of the phrase’s last quarter minus the
  phrase’s median, then the median over phrases: negative is a falling ending
  (a flat, final statement), positive a rising one (a question, a setup that
  is not finished). `f0_median_hz` is reported for reference, and
  `register_st` — the clip’s median pitch in semitones above (or below) a
  `reference_hz` the caller passes, typically the same voice’s neutral
  median — measures a register jump (a character voice, an outburst); it is
  `nan` without a reference.
- **Loudness.** `loudness_range_db`: the 90th minus the 10th percentile of
  the speech frames’ level.
- **Emphasis.** `emphasis_per_s`: syllable-rate peaks of the level envelope
  that stand out — at least `emphasis_db` above the clip’s median speech level
  or `emphasis_st` above its median pitch — per second of speaking time.

**Lines, not performances.** Targets measured per sentence (a range of
per-sentence values) are compared with the median over a set of lines, each
measured alone ([`measure_lines()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.measure_lines), the CLI’s default for several files):
joining lines first spreads the pitch statistics by the register changes
between them.

These are estimators, crude on purpose: numpy only, no model, deterministic.
A target measured with them is only comparable to a clip measured with them,
so change an estimator only together with re-measuring the targets that use it.

The core is a pure function over samples, testable without ffmpeg:

```pycon
>>> import numpy as np
>>> sr = 16000
>>> t = np.arange(int(0.5 * sr)) / sr
>>> tone = 0.3 * np.sin(2 * np.pi * 150 * t)
>>> clip = np.concatenate([tone, np.zeros(int(0.4 * sr)), tone])
>>> m = measure_prosody(clip, sr, text="one two")
>>> round(m.f0_median_hz), m.pauses, round(m.pause_median_s, 2)
(150, 1, 0.4)
>>> m.syllables, round(m.articulation_rate_sps, 1)
(2, 2.0)
```

A target is a `[low, high]` range; a miss is a warning, and a target nothing
measures is refused:

```pycon
>>> check_prosody(m, {"pauses_per_min": [0, 200]})
[]
>>> check_prosody(m, {"pauses_per_min": [0, 10]})[0].ir_path
'<prosody>/pauses_per_min'
>>> check_prosody(m, {"swagger": [0, 1]})
Traceback (most recent call last):
...
an.verify.prosody.ProsodyTargetError: unknown prosody target 'swagger'; measurable targets are [...]
```

### Module Attributes

| [`ESTIMATOR_VERSION`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ESTIMATOR_VERSION)   | Raised whenever an estimator or one of its defaults below changes what a clip measures.   |
|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`METRICS`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.METRICS)             | The measurable targets, and what each one is.                                             |

### Functions

| [`measure_prosody`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.measure_prosody)(samples, sr, \*[, text, ...])   | Measure one clip's delivery (see the module docstring for each metric).                                                                                                                                       |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`measure_prosody_file`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.measure_prosody_file)(paths, \*[, text, sr])     | Measure one audio file, or several joined by [`join_speech()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.join_speech) (one performance).                                                                                |
| [`measure_lines`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.measure_lines)(paths, \*[, texts, sr])           | Measure each file on its own (with its own text) and return the [`median_stats()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.median_stats) — the comparison for targets measured per sentence.                           |
| [`median_stats`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.median_stats)(stats)                             | The per-field median of several clips' stats (`nan` ignored); counts and durations are summed.                                                                                                                |
| [`pitch_track`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.pitch_track)(samples, sr, \*[, fmin, fmax, ...]) | A YIN pitch track: Hz per hop, `nan` where no period is found.                                                                                                                                                |
| [`count_syllables`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.count_syllables)(text)                           | An English syllable estimate: vowel groups per word, a silent final `e` dropped, at least one per word; a number counts one per digit.                                                                        |
| [`join_speech`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.join_speech)(clips, sr, \*[, gap_s, ...])        | Clips with their leading and trailing silence trimmed, joined with `gap_s` of silence — so a set of lines measures as one performance without the authored gaps between lines counting as the voice's pauses. |
| [`decode_audio`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.decode_audio)(path, \*[, sr, ffmpeg])            | Any audio file ffmpeg reads, as mono float32 samples at `sr` Hz.                                                                                                                                              |
| [`check_prosody`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.check_prosody)(stats, targets, \*[, ...])        | One finding per target `stats` misses (an `info` for one it cannot measure).                                                                                                                                  |
| [`target_distance`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.target_distance)(stats, targets, \*[, ...])      | How far `stats` sits from `targets`: `(outside, off_centre)`, lower is closer.                                                                                                                                |
| [`validate_targets`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.validate_targets)(targets)                       | Raise [`ProsodyTargetError`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyTargetError) unless every target names a metric and is a `[low, high]` range.                                                                    |
| [`prosody_lint`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.prosody_lint)(audio, targets, \*[, text, ...])   | Measure `audio` (a path, several paths, or stats already measured) and report each target it misses.                                                                                                          |

### Classes

| [`ProsodyStats`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyStats)(duration_s, span_s, speech_s, ...)   | One clip's delivery, in the units [`METRICS`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.METRICS) describes.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|

### Exceptions

| [`ProsodyTargetError`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyTargetError)   | A prosody target names no metric, or is not a `[low, high]` range.   |
|-----------------------------------------------------------------------|----------------------------------------------------------------------|
| [`ProsodyDecodeError`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyDecodeError)   | An audio file could not be decoded (ffmpeg missing or failing).      |

### an.verify.prosody.ESTIMATOR_VERSION *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '1'*

Raised whenever an estimator or one of its defaults below changes what a clip
measures. Anything that persists a choice made from these numbers (the best
take of a line, [`an.audio.takes`](_autosummary/an.audio.takes.html.md#module-an.audio.takes)) keys on it, so a changed estimator
re-chooses instead of trusting numbers it would no longer produce.

### an.verify.prosody.METRICS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'articulation_rate_sps': 'syllables per second of speaking time (needs the text)', 'emphasis_per_s': 'stand-out level peaks per second of speaking time', 'f0_median_hz': 'median pitch (voice-specific; for reference)', 'f0_range_st': 'pitch range, 5th to 95th percentile, semitones', 'f0_sd_st': 'pitch spread, semitones about the median', 'final_drop_st': 'median phrase ending relative to its phrase, semitones (negative falls)', 'loudness_range_db': 'speech level, 90th minus 10th percentile, dB', 'pause_median_s': 'median pause length', 'pause_p90_s': '90th-percentile pause length', 'pause_share': 'share of the span that is pauses', 'pauses_per_min': 'pauses per minute of span', 'register_st': 'median pitch relative to reference_hz, semitones (needs a reference)', 'speech_rate_sps': "syllables per second of the clip's span (needs the text)", 'voiced_share': 'share of speaking time with a pitch'}*

The measurable targets, and what each one is.

### *exception* an.verify.prosody.ProsodyDecodeError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

An audio file could not be decoded (ffmpeg missing or failing).

### *class* an.verify.prosody.ProsodyStats(duration_s, span_s, speech_s, syllables, pauses, articulation_rate_sps, speech_rate_sps, pause_share, pauses_per_min, pause_median_s, pause_p90_s, f0_median_hz, f0_sd_st, f0_range_st, final_drop_st, loudness_range_db, emphasis_per_s, voiced_share, register_st=nan)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One clip’s delivery, in the units [`METRICS`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.METRICS) describes.

`nan` means “not measurable on this clip” (no text for a rate, no pause
for a pause length, no voiced phrase for a contour).

#### as_dict(, ndigits=3)

The stats as a plain dict, floats rounded (`nan` becomes `None`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### *exception* an.verify.prosody.ProsodyTargetError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A prosody target names no metric, or is not a `[low, high]` range.

### an.verify.prosody.check_prosody(stats, targets, , miss_severity='warning', path_prefix='<prosody>')

One finding per target `stats` misses (an `info` for one it cannot measure).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/an.verify.html.md#an.verify.Finding)]

### an.verify.prosody.count_syllables(text)

An English syllable estimate: vowel groups per word, a silent final `e`
dropped, at least one per word; a number counts one per digit.

Bracketed audio tags (`[sighs]`) are not words and are skipped.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> count_syllables("He did not do the job.")
6
>>> count_syllables("[deadpan] Forty-seven criteria.")  # truly 8: an estimate
7
```

### an.verify.prosody.decode_audio(path, , sr=16000, ffmpeg='ffmpeg')

Any audio file ffmpeg reads, as mono float32 samples at `sr` Hz.

* **Return type:**
  `ndarray`

### an.verify.prosody.join_speech(clips, sr, , gap_s=0.0, speech_floor_db=30.0, hop_s=0.01, window_s=0.025)

Clips with their leading and trailing silence trimmed, joined with `gap_s`
of silence — so a set of lines measures as one performance without the
authored gaps between lines counting as the voice’s pauses.

* **Return type:**
  `ndarray`

```pycon
>>> sr = 1000
>>> c = np.concatenate([np.zeros(200), np.ones(300), np.zeros(200)])
>>> 0.6 <= len(join_speech([c, c], sr)) / sr < 0.7  # edges blur by one window
True
```

### an.verify.prosody.measure_lines(paths, , texts=None, sr=16000, \*\*knobs)

Measure each file on its own (with its own text) and return the
[`median_stats()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.median_stats) — the comparison for targets measured per sentence.

* **Return type:**
  [`ProsodyStats`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyStats)

### an.verify.prosody.measure_prosody(samples, sr, , text=None, hop_s=0.01, window_s=0.025, fmin=60.0, fmax=500.0, speech_floor_db=30.0, min_pause_s=0.12, phrase_pause_s=0.25, emphasis_db=6.0, emphasis_st=3.0, peak_gap_s=0.12, final_share=0.25, reference_hz=None)

Measure one clip’s delivery (see the module docstring for each metric).

`samples` is mono audio at `sr` Hz (any scale); `text` is what is said,
used only to count syllables for the two rates; `reference_hz` is the pitch
`register_st` is measured from.

* **Return type:**
  [`ProsodyStats`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyStats)

### an.verify.prosody.measure_prosody_file(paths, , text=None, sr=16000, \*\*knobs)

Measure one audio file, or several joined by [`join_speech()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.join_speech) (one
performance). `text` is the words of all of them together.

Joining moves the pitch statistics: each line has its own register, so a
joined set spreads wider than any of its lines. To compare lines with
per-sentence targets, use [`measure_lines()`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.measure_lines).

* **Return type:**
  [`ProsodyStats`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyStats)

### an.verify.prosody.median_stats(stats)

The per-field median of several clips’ stats (`nan` ignored); counts
and durations are summed. How a set of lines is compared with targets
measured per sentence.

* **Return type:**
  [`ProsodyStats`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyStats)

```pycon
>>> a = ProsodyStats(1.0, 1.0, 1.0, 3, 0, *[1.0] * 13)
>>> b = ProsodyStats(2.0, 2.0, 2.0, 5, 1, *[3.0] * 13)
>>> m = median_stats([a, b])
>>> m.f0_sd_st, m.syllables, m.span_s
(2.0, 8, 3.0)
```

### an.verify.prosody.pitch_track(samples, sr, , fmin=60.0, fmax=500.0, hop_s=0.01, window_s=0.025, threshold=0.1, voicing_threshold=0.35, chunk=4096)

A YIN pitch track: Hz per hop, `nan` where no period is found.

* **Return type:**
  `ndarray`

```pycon
>>> sr = 16000
>>> t = np.arange(sr // 2) / sr
>>> f0 = pitch_track(np.sin(2 * np.pi * 220 * t), sr)
>>> round(float(np.nanmedian(f0)))
220
>>> bool(np.isnan(pitch_track(np.zeros(sr // 4), sr)).all())
True
```

### an.verify.prosody.prosody_lint(audio, targets, , text=None, miss_severity='warning')

Measure `audio` (a path, several paths, or stats already measured) and
report each target it misses. Passes unless `miss_severity` is `error`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`ProsodyStats`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyStats), [`VerificationReport`](_autosummary/an.verify.html.md#an.verify.VerificationReport)]

### an.verify.prosody.target_distance(stats, targets, , unmeasurable=1.0)

How far `stats` sits from `targets`: `(outside, off_centre)`, lower is closer.

`outside` sums, over the targets, the distance outside `[low, high]` in
units of the range’s width (0 for a value inside); a metric this clip cannot
measure counts `unmeasurable` widths, so a broken take never wins by
having no number. `off_centre` sums each value’s distance from its range’s
midpoint, in the same units — the tie-break between takes that are all on
target. A zero-width range counts as one unit wide.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> s = ProsodyStats(1.0, 1.0, 1.0, 3, 0, *[3.0] * 13)
>>> target_distance(s, {"f0_sd_st": [2, 4]})
(0.0, 0.0)
>>> target_distance(s, {"f0_sd_st": [4, 6], "f0_range_st": [1, 5]})
(0.5, 1.0)
```

### an.verify.prosody.validate_targets(targets)

Raise [`ProsodyTargetError`](_autosummary/an.verify.prosody.html.md#an.verify.prosody.ProsodyTargetError) unless every target names a metric and is a `[low, high]` range.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> validate_targets({"f0_sd_st": [2, 4]})
>>> validate_targets({"f0_sd_st": [4, 2]})
Traceback (most recent call last):
...
an.verify.prosody.ProsodyTargetError: prosody target 'f0_sd_st' must be [low, high], got [4, 2]
```


# _autosummary/an.verify.vision.html.md

# an.verify.vision

VisionLMVerifier — Claude vision looks at sampled frames and reports issues.

**Opt-in, not default.** `an.orchestrate.orchestrate`’s default verifier chain
is `[LayoutLintVerifier(), MediaQualityVerifier()]` and this verifier is
deliberately not in it: it costs money and its input is a rendered mp4. The
lazy `anthropic` import and the key check therefore skip *cleanly*, so a caller
without the `vision` extra gets an informational Finding rather than an
`ImportError`. (The docstring here previously claimed the opposite — that the
skips existed “so the orchestrator can keep this verifier in its default
chain”. Nothing in `an/` has ever put it in one.)

**Not configured is a skip. Configured and broken is a failure.** That
distinction is the whole of an#39. Every failure path used to add an `info`
Finding and return, and `VerificationReport.add` flips `passed` only on
`"error"` — so a dead model id, a 500, a refusal and an unparseable reply all
came back byte-identical to a clean bill of health. A verifier that reports
success when it failed to run is worse than no verifier, because it launders an
absence of evidence into evidence of absence.

So the paid call lives behind an injectable seam, [`judge_frames()`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_frames), which
takes **frame bytes** (not paths — the real frames live in a
`TemporaryDirectory`, so a cache key over paths would miss 100% of the time)
and returns the model’s **raw text** (so `_parse_issues` stays outside any
recording and parser fixes are testable against it for free). A judge that
cannot answer raises [`VisionJudgeError`](_autosummary/an.verify.vision.html.md#an.verify.vision.VisionJudgeError); a cassette that has no
recording for a call raises [`CassetteMiss`](_autosummary/an.verify.vision.html.md#an.verify.vision.CassetteMiss), which derives from
**\`BaseException\`** because every other kind is swallowed twice on the way out
— once by this module’s own handler and once by `orchestrate`’s broad
post-render `except Exception`, which guards every verifier and must stay
broad.

Cost: one Anthropic call carrying `frame_count` base64 PNGs plus a short
prompt. Roughly $0.005 with Haiku.

### Module Attributes

| [`FAILURE_SEVERITY`](_autosummary/an.verify.vision.html.md#an.verify.vision.FAILURE_SEVERITY)   | Severity for "configured, called, no verdict" — a failed call, or a reply that carried no verdict.   |
|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|

### Functions

| [`emotion_prompt`](_autosummary/an.verify.vision.html.md#an.verify.vision.emotion_prompt)(labels)                           | The name-the-emotion prompt over a closed label set.                                                       |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| [`judge_emotion`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_emotion)(frames, \*[, labels, judge, ...])  | Name the emotion a frame (or short strip) shows, or `None` if the reply named nothing in `labels` (an#98). |
| [`judge_envelope`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_envelope)(frames, \*[, prompt, ...])        | Call the vision model and return a recordable envelope.                                                    |
| [`judge_frames`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_frames)(frames, \*[, prompt, ...])          | The paid seam: frame bytes in, the model's raw text reply out.                                             |
| [`judge_key`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_key)(\*args, \*\*kwargs)                    | The cache key for one [`judge_frames()`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_frames) call.                |
| [`judge_legibility`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_legibility)(frames, text, \*[, judge, ...]) | Score a dense in-line frame strip for lip-sync legibility (an#97).                                         |
| [`legibility_prompt`](_autosummary/an.verify.vision.html.md#an.verify.vision.legibility_prompt)(text)                          | The legibility prompt for one line.                                                                        |

### Classes

| [`VisionLMVerifier`](_autosummary/an.verify.vision.html.md#an.verify.vision.VisionLMVerifier)(\*[, model, frame_count, ...])   | Claude vision Verifier (skip-if-missing-deps).   |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------|

### Exceptions

| [`CassetteMiss`](_autosummary/an.verify.vision.html.md#an.verify.vision.CassetteMiss)     | A recorded reply was asked for and there is none.              |
|-------------------------------------------------------------------|----------------------------------------------------------------|
| [`VisionJudgeError`](_autosummary/an.verify.vision.html.md#an.verify.vision.VisionJudgeError) | The judge was configured, was called, and produced no verdict. |

### *exception* an.verify.vision.CassetteMiss

Bases: [`BaseException`](https://docs.python.org/3/builtins/exceptions.html#BaseException)

A recorded reply was asked for and there is none.

Derived from `BaseException` rather than `Exception`, deliberately and
for a measured reason: an `Exception` raised where the API call sits is
caught by this module’s own handler AND by `an.orchestrate`’s post-render
`except Exception`, which guards every verifier and must stay broad. Both
of them report and continue, so a test asserting “this run did not spend”
would pass having verified nothing at all.

Same reasoning, and the same shape, as `tests/conftest.py`’s
`OutboundNetworkAttempt` — whose own docstring names “the verifiers’ broad
handlers” as the reason.

### an.verify.vision.FAILURE_SEVERITY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'warning'*

Severity for “configured, called, no verdict” — a failed call, or a reply
that carried no verdict.

`warning`, not `error`: a transient 529 must not fail a whole render. And
not `info`, which is the NOT-CONFIGURED severity — reusing it for
“configured and broken” is exactly what made this verifier invisible. Any
non-`info` Finding makes a dead verifier show up in the report.

Set it to `"error"` if a refusal should fail the render; that is a policy
choice, and it is one constant.

### *exception* an.verify.vision.VisionJudgeError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The judge was configured, was called, and produced no verdict.

An `an`-owned type on purpose. Narrowing the catch site to
`anthropic.APIError` looks tighter and is not:
`issubclass(anthropic.NotFoundError, anthropic.APIError)` is True, so a
dead model id would still be swallowed into a pass — and it would put a
vendor class at a catch site in this package’s own control flow.

### *class* an.verify.vision.VisionLMVerifier(, model='claude-haiku-4-5-20251001', frame_count=4, max_tokens=800, api_key=None, judge=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Claude vision Verifier (skip-if-missing-deps).

#### judge

Injected, per “no globals, no service locators” — and because
injection is what makes record-vs-replay drift impossible: the
recorded and replayed paths are the same call through the same
object, differing only in what the store returns.

### an.verify.vision.emotion_prompt(labels)

The name-the-emotion prompt over a closed label set. The labels are part
of the key, so a different set is a different recording.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> "happy" in emotion_prompt(["neutral", "happy"])
True
```

### an.verify.vision.judge_emotion(frames, , labels=None, judge=None, model='claude-haiku-4-5-20251001', max_tokens=800, api_key=None)

Name the emotion a frame (or short strip) shows, or `None` if the
reply named nothing in `labels` (an#98).

`labels` defaults to every expression preset the loaded genre knows (the
`expression.known_presets` service; `cutan` in practice). `judge` is
the `judge_frames`-shaped seam; parsing stays outside the recording.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.verify.vision.judge_envelope(frames, \*, prompt='You are reviewing frames from a short animated cartoon. The character\\\\nart is intentionally simple (placeholder geometry: ellipse heads, rect\\\\ntorsos/limbs, curved bezier mouths, eyes drawn as white-sclera + dark\\\\npupils). DO NOT comment on the simplicity of the art itself — that is by\\\\ndesign. DO comment on: \\\\n\\\\n- Characters that are clipped off-screen or overlap badly.\\\\n- Faces that are missing parts (no eyes, mouth not visible, head occluded).\\\\n- Motion that looks broken (limbs detached, character flying off-canvas).\\\\n- Mouth shape that obviously doesn\\\\'t match active speech (e.g. closed lips\\\\n  during a long word).\\\\n- Background obscuring a character.\\\\n\\\\nReply in JSON only, with this shape: \\\\n\\\\n{\\\\n  "issues": [\\\\n    {"severity": "warning"|"error", "where": '<short location hint>', "what": "<one sentence>"}\\\\n  ]\\\\n}\\\\n\\\\nIf everything looks fine, return \`\`{"issues": []}\`\`.\\\\n', model='claude-haiku-4-5-20251001', max_tokens=800, api_key=None)

Call the vision model and return a recordable envelope.

Memoized one level *in* from [`judge_frames()`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_frames) because a memoizer hands
the store only `(key, return_value)` — so a seam returning a bare `str`
can record nothing beside the reply, and the provenance that makes a
cassette auditable would be unwritable.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### an.verify.vision.judge_frames(frames, \*, prompt='You are reviewing frames from a short animated cartoon. The character\\\\nart is intentionally simple (placeholder geometry: ellipse heads, rect\\\\ntorsos/limbs, curved bezier mouths, eyes drawn as white-sclera + dark\\\\npupils). DO NOT comment on the simplicity of the art itself — that is by\\\\ndesign. DO comment on: \\\\n\\\\n- Characters that are clipped off-screen or overlap badly.\\\\n- Faces that are missing parts (no eyes, mouth not visible, head occluded).\\\\n- Motion that looks broken (limbs detached, character flying off-canvas).\\\\n- Mouth shape that obviously doesn\\\\'t match active speech (e.g. closed lips\\\\n  during a long word).\\\\n- Background obscuring a character.\\\\n\\\\nReply in JSON only, with this shape: \\\\n\\\\n{\\\\n  "issues": [\\\\n    {"severity": "warning"|"error", "where": '<short location hint>', "what": "<one sentence>"}\\\\n  ]\\\\n}\\\\n\\\\nIf everything looks fine, return \`\`{"issues": []}\`\`.\\\\n', model='claude-haiku-4-5-20251001', max_tokens=800, api_key=None)

The paid seam: frame bytes in, the model’s raw text reply out.

`bytes` rather than `Path` because the real frames live in a
`TemporaryDirectory`, so a key over paths hashes a fresh random string and
misses every time. **Raw text** rather than parsed findings because that
keeps `_parse_issues` outside any recording — a parser fix is then testable
against the recording for free, and record-vs-replay drift is impossible.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### an.verify.vision.judge_key(\*args, \*\*kwargs)

The cache key for one [`judge_frames()`](_autosummary/an.verify.vision.html.md#an.verify.vision.judge_frames) call.

Derived from the seam’s **signature**, not from a hand-written allowlist.
An allowlist sets the default to *exclude*, which means a parameter added
later collides with the base key and the recording is served forever for a
request that changed. A false miss is red CI; a false hit is silent and
unrecoverable, so the default has to be *include*.

`apply_defaults()` matters too: without it `judge_frames(frames)` and
the fully-spelled call are two different keys for one request.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> k = judge_key([b"a"], model="m", max_tokens=1, prompt="p")
>>> k == judge_key([b"a"], model="m", max_tokens=1, prompt="p", api_key="secret")
True
>>> k == judge_key([b"a"], model="m", max_tokens=1, prompt="  p  ")
True
>>> k == judge_key([b"b"], model="m", max_tokens=1, prompt="p")
False
```

### an.verify.vision.judge_legibility(frames, text, , judge=None, model='claude-haiku-4-5-20251001', max_tokens=800, api_key=None)

Score a dense in-line frame strip for lip-sync legibility (an#97).

`judge` is the `judge_frames`-shaped seam — the cassette-backed one in
tests, the paid one otherwise. Parsing stays outside the recording.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### an.verify.vision.legibility_prompt(text)

The legibility prompt for one line. The text is part of the key, so a
different line is a different recording.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-14 05:06 UTC** from commit <a href="https://github.com/reeleehq/an/commit/521931784f348e169d84181956bc597f79749ea1"><code>5219317</code></a> on branch <code>main</code>, for **an 0.1.239** (from <code>pyproject.toml</code>).

#### WARNING
The documentation and the package may be misaligned:

- The documented version (0.1.239) is ahead of the latest release on PyPI (0.1.238): these docs describe unreleased code.

## Source

|                     |                                                                                                                                                    |
|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/reeleehq/an/commit/521931784f348e169d84181956bc597f79749ea1"><code>521931784f348e169d84181956bc597f79749ea1</code></a> |
| Branch              | <code>main</code>                                                                                                                                  |
| Tags at this commit | <code>0.1.239</code>                                                                                                                               |
| Working tree        | clean                                                                                                                                              |
| Remote              | <code>https://github.com/reeleehq/an</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>reeleehq/an</code>                                                                   |
| Run          | <a href="https://github.com/reeleehq/an/actions/runs/38514283420">38514283420</a>          |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>e32818a53c6d5d1e67b07dac51aaeb0501f5c133</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.17  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.15 |

## Configuration as resolved

|               |                                                                   |
|---------------|-------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>pydata_sphinx_theme</code>) |
| accent        | <code>#3552a0</code>                                              |
| api_generator | <code>autosummary</code>                                          |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>  |
| agent_outputs | <code>true</code>                                                 |
| aggregates    | <code>md</code>                                                   |
| ai_artifacts  | <code>true</code>                                                 |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/an/0.1.238/">0.1.238</a>, older than the documented version (0.1.239).

## Reproduce

```bash
git clone https://github.com/reeleehq/an && cd an
git checkout 521931784f348e169d84181956bc597f79749ea1
pip install "epythet==0.2.17"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`an` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `an`

Use whenever the user wants to author, edit, render, or iterate on a structured animation, cartoon, explainer video, or motion graphic via the an Python package. Triggers on “make a cartoon”, “animate”, “render a scene”, “let’s build a video”, “an init”, “an validate”, “an render”, or any request that maps to the chat-driven director workflow.

Source: [`.claude/skills/an`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an).

### `an-art-package`

Moved to the `cutan-art-package` skill in the cutan repository (an#225). Use `cutan-art-package` for the artist-facing contract for a cut-out character and how to check a delivery. Triggers on the old name.

Source: [`.claude/skills/an-art-package`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-art-package).

### `an-dev`

Use when working *inside* the an repo itself — writing or modifying an’s own Python code, tests, schemas, adapters, or skills. Triggers when CWD is the an repo and the task is implementation work (adding a renderer, fixing a schema bug, updating a store). Not for using an from a downstream project.

Source: [`.claude/skills/an-dev`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev).

### `an-dev-bench`

Use when working on `an bench`, the metrics ledger, the golden corpus, or anything that measures rendered output in the `an` repo. Triggers on “add a metric”, “the ledger”, “an bench”, “golden frames”, “mutation test the harness”, “why is this number moving”, “bench corpus”, or any change to `an/bench/`, `misc/bench/`, or the encode/decode flags in `an/media/mp4.py` (formerly `an/stage/render.py`), or the frame stage in `an/engines/`.

Source: [`.claude/skills/an-dev-bench`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-bench).

### `an-dev-expression`

Moved to the `cutan-dev-expression` skill in the cutan repository (an#225). Use `cutan-dev-expression` for the cut-out face: expression presets, bindings, blinks, gaze. Triggers on the old name.

Source: [`.claude/skills/an-dev-expression`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-expression).

### `an-dev-licensing`

Use before adding, upgrading, vendoring or swapping ANY third-party dependency, model weight, font, art asset or JS bundle in the an repo — and before writing any licence claim into a doc, comment or PR body. Also use when choosing between two libraries that solve the same problem, when a search result recommends the obvious package, and when deciding whether an asset an end user’s video will contain may ship. Triggers on “add a dependency”, “which library”, “vendor”, “is X permissive”, “can we use”, “licence”, “license”, “attribution”, “model weights”.

Source: [`.claude/skills/an-dev-licensing`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-licensing).

### `an-dev-lipsync`

Moved to the `cutan-dev-lipsync` skill in the cutan repository (an#225). Use `cutan-dev-lipsync` for cut-out lip-sync: providers, visemes and coarticulation. Triggers on the old name.

Source: [`.claude/skills/an-dev-lipsync`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-lipsync).

### `an-dev-path`

Stroked paths in the `an` repo (an#160, epic

Source: [`.claude/skills/an-dev-path`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-path).

### `an-dev-render-pipeline`

The frame path in the `an` repo, end to end — Pixi rasterisation, Playwright element capture, the PNG stage, the x264 mux, concat and delivery — and what each stage can lose. Use when changing anything that touches a pixel or an encode flag - supersampling, resolution, antialias, `device_scale_factor`, `autoDensity`, downscale filters, `-pix_fmt` / CRF / preset / colour tags, `_capture_frames`, `_ffmpeg_mux`, `an/engines/` (the frame stage, `capture_frames`, `frame_stage_renderer`), `an/media/` (`mp4.py`, `supersample.py`, `shutter.py`, sinks), `_ffmpeg_concat`, `runtime.js`’s PIXI.Application options, or the per-shot mp4 store. Triggers on “supersample”, “why is the render soft”, “add an encoder flag”, “make it render bigger”, “downscale”, “4:4:4”, “faststart”, “the frames look wrong”, “speed up the render”.

Source: [`.claude/skills/an-dev-render-pipeline`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-render-pipeline).

### `an-dev-rig-contract`

Moved to the `cutan-dev-rig-contract` skill in the cutan repository (an#225). Use `cutan-dev-rig-contract` for the cut-out rig contract: descriptor, skeleton, slots and skins. Triggers on the old name.

Source: [`.claude/skills/an-dev-rig-contract`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-rig-contract).

### `an-dev-runtime-assets`

Use when adding, moving, renaming or removing any non-Python file that the renderer loads at run time — anything under `an/stage/runtime/` (the stage’s runtime, `an/data/cutout_runtime/` before an#247) or `an/data/`, a vendored JS bundle, an SVG part, a font, an HTML page. Also use when a render looks wrong rather than failing, when something works in the dev tree but not from a `pip install`, or when a “fix” to a data file appears to have no effect. Triggers on “vendor”, “bundle”, “asset”, “force-include”, “packaging”, “it works locally but not installed”.

Source: [`.claude/skills/an-dev-runtime-assets`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-runtime-assets).

### `an-dev-stage`

The stage in the `an` repo — the camera (which is `root.pivot`, and already exists), parallax as one compile-time factor per plane, the multiplane environment descriptor, props, the StylePack, and the rename `Shot.style` → `Shot.renderer`. Load before touching `_add_camera_clips`, `_build_environment_subtree`, `_build_scene_root`, `Camera`, `AssetRef.stage`, the environments/styles/props stores, or any scene that pans. Triggers on “camera”, “pan”, “parallax”, “tilt”, “perspective”, “rotation_x”, “crawl”, “multiplane”, “plane”, “environment”, “backdrop”, “prop”, “style pack”, “art direction”, “renderer selector”.

Source: [`.claude/skills/an-dev-stage`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-stage).

### `an-dev-swap-channels`

Moved to the `cutan-dev-swap-channels` skill in the cutan repository (an#225). Use `cutan-dev-swap-channels` for whole-character swaps, `swap_poses`, and the view/eyelid/viseme conventions (the generic swap mechanism itself is in `an-dev-stage`). Triggers on the old name.

Source: [`.claude/skills/an-dev-swap-channels`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-swap-channels).

### `an-dev-text`

Text on screen in the `an` repo (an#155, an#175, epic

Source: [`.claude/skills/an-dev-text`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-dev-text).

### `an-spec`

Use when the user is starting a new an scene and needs help dialogically developing a scene specification — clarifying questions about characters, dialogue, art style, voices, pacing, and camera. Triggers on “let’s design a scene”, “I want to make a video about…”, “help me sketch a cartoon”, or any open-ended creative request handed to an.

Source: [`.claude/skills/an-spec`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-spec).

### `an-style`

Moved to the `cutan-style` skill in the cutan repository (an#225). Use `cutan-style` for applying a named cut-out style (South Park, OverSimplified, Kurzgesagt, Gilliam, Reiniger, Norstein) to a scene and linting the render. Triggers on the old name.

Source: [`.claude/skills/an-style`](https://github.com/thorwhalen/an/tree/HEAD/.claude/skills/an-style).

## Instruction files

Files agents read before working in this repository.

- [`CLAUDE.md`](https://github.com/thorwhalen/an/tree/HEAD/CLAUDE.md): read by Claude Code

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://thorwhalen.github.io/an/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`an.md`](https://thorwhalen.github.io/an/an.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://thorwhalen.github.io/an/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`an`](_autosummary/an.html.md#module-an)   | an — AI-driven structured animation.   |
|-----------------------------------------------------------------|----------------------------------------|


