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([scenes, out, keep_render, quiet, ...])

Render the fixed bench corpus and write a metrics ledger.

bench_compare([before, after, mutation, ...])

Compare two ledger rows — and refuse when they are not comparable.

bench_mutants([names, quiet])

Break each guard on purpose and check the test that names it goes red.

check()

Print a status report of all backend system + Python deps.

credits(project_dir[, json_out])

Show what third-party work is in project_dir and what it obliges.

init(project_dir[, name, force, id, genre, ...])

Create a fresh an project at project_dir — or, with --id, at the default location.

iterate(project_dir, instruction[, ...])

Apply a free-text instruction to the scene.

preview(project_dir[, shot, no_browser])

Live-preview the project's scene in a browser; reloads on edit.

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()

The sub-namespaces installed genres added (cli.<namespace> services).

render(project_dir[, output_name, tts, ...])

Render the project at project_dir to a single mp4.

sync(project_dir[, accept_measured])

Reconcile scene.md and ir/scene.json inside project_dir.

validate(project_dir[, fps, strict_assets])

Validate the scene at project_dir.

an.tools.bench(scenes='', out='', keep_render='', quiet=False, bless='', compare='', mutation='')[source]#

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

an.tools.bench_compare(before='', after='', mutation='', strict=False, raw=False)[source]#

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

an.tools.bench_mutants(names='', quiet=False)[source]#

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

an.tools.check()[source]#

Print a status report of all backend system + Python deps.

Return type:

str

an.tools.credits(project_dir, json_out=None)[source]#

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

an.tools.init(project_dir, name=None, force=False, id=False, genre='', package='', root='')[source]#

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

an.tools.iterate(project_dir, instruction, apply_changes=True, model='claude-opus-4-7')[source]#

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

an.tools.preview(project_dir, shot='', no_browser=False)[source]#

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

an.tools.probe(project_dir, shot='', at=None, out='', columns=0, supersample=1, allow_private_here=False)[source]#

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

an.tools.registered_namespaces()[source]#

The sub-namespaces installed genres added (cli.<namespace> services).

Return type:

dict[str, list]

>>> 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)[source]#

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

an.tools.sync(project_dir, accept_measured=False)[source]#

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

an.tools.validate(project_dir, fps=0.0, strict_assets=False)[source]#

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