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.
- exception an.bench.capture.CaptureError[source]
A capture could not produce something the metrics need.
- an.bench.capture.FRAME_PNG_GLOB: str = 'frame_*.png'
How a shot’s frames are named on disk. One constant rather than the literal repeated at each glob site.
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.
- an.bench.capture.IGNORED_ON_COPY: tuple[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_patternsdoes, and it is whyartifacts/shotscannot be spelled here. SeeIGNORED_RELPATHS_ON_COPY.- Type:
Copied for the render, but never these
- an.bench.capture.IGNORED_RELPATHS_ON_COPY: tuple[str, ...] = ('artifacts/shots',)
Excluded by their path relative to the project root, POSIX-spelled.
mall["shots"]is<project>/artifacts/shots, andartifacts/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, and both fail silently.shutil.ignore_patternsreturns a closure handed the NAMES inside one directory, which itfnmatch.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.capture.RENDER_WORK_RELPATH: str = '.an/render_work'
Where the renderer leaves its per-shot working tree inside the project.
- class an.bench.capture.SceneCapture(name: str, source: str, prepared: bool, project_dir: ~pathlib.Path, mp4: ~pathlib.Path, shots: list[~an.bench.capture.ShotCapture], resolution: tuple[int, int], fps: int, duration: float, n_declared_entity_refs: int, visual_kinds: set[str], asset_resolution: list[dict], audio_cache: str, wall_seconds: float, determinism: dict = <factory>)[source]
One fixture’s whole render.
- class an.bench.capture.ShotCapture(shot_id: str, frames_dir: Path, scene_json: dict, runtime_dir: Path, frame_count: int, duration: float = 0.0, frame_sizes: tuple[tuple[int, int], ...] = ())[source]
One rendered shot’s artifacts.
- duration: 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[tuple[int, 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’smetaand never from a file. Empty only when the shot wrote no frames.
- an.bench.capture.capture_fixture(name: str, fixture: Fixture, *, repo_root: Path, keep_render: Path | None = None) SceneCapture[source]
Render
fixturein 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.
- an.bench.capture.cleanup(capture: SceneCapture) None[source]
Remove a capture’s throwaway tree.
- an.bench.capture.dirty_paths(repo_root: Path) list[str][source]
git status –porcelain lines, so a capture can prove it touched nothing.
Raises
GitStatusUnavailablewhen git does not answer, rather than reporting a clean tree it never observed.
- an.bench.capture.distinct_png_sizes(frames_dir: Path) tuple[tuple[int, int], ...][source]
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.pypatchesruntime.jstoresolution: k, autoDensity: falseand drivescapture_fixture()directly to measure the supersample — so this module reports what it saw andan.bench.runis where the bench’s invariant is asserted.
- an.bench.capture.expected_frame_count(duration: float, fps: int) int[source]
The renderer’s own frame-count expression, reused rather than restated.
max(1, int(round(duration * fps)))— and Python 3’sroundis banker’s rounding, somath.ceilorint(x + 0.5)silently disagrees on every half-frame duration.>>> expected_frame_count(2.5, 24) 60 >>> expected_frame_count(0.0, 24) 1
- an.bench.capture.stage_copy(fixture_dir: Path, base: Path) Path[source]
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 contiguousframe_%06d.pngrun 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_COPYby basename at any depth, andIGNORED_RELPATHS_ON_COPYby path from the project root — which is the only way to dropartifacts/shotswhile keepingartifacts/audio.