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:
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), 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 and an.bench.capture (genre), which re-export
them. Nothing here imports the stage at module level.
>>> 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
Rendering knobs pinned for every bench capture, recorded verbatim into the ledger. |
|
Where the bench-owned fixtures live. |
|
The core corpus (see the module docstring). |
|
they are the previous render's output, and one of them silently extends this one's frame sequence. |
|
Excluded by their path relative to the project root, POSIX-spelled. |
|
Where the renderer leaves its per-shot working tree inside the project. |
|
How a shot's frames are named on disk. |
|
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. |
|
Where an assembled scene's composed frames are written, under the render's work directory, by |
Functions
|
The |
|
An assembled scene's composed frames, written under |
|
Where a render left the frames of |
|
|
|
Render |
|
Copy a fixture into |
Classes
|
A corpus scene: where it lives, how to build it, what it must render. |
Exceptions
A capture could not produce something the metrics need. |
- an.bench.core_corpus.BENCH_RENDER_KWARGS: dict[str, 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=Falsebecause audio cannot move a pixel and would otherwise make the frames depend on the audio cache’s warm/cold state;parallel=1because a timing-sensitive pool is one more thing to explain if the pixels ever do differ;strict_assets=Truebecause a stand-in asset renders happily as a DIFFERENT picture (an#33).
- an.bench.core_corpus.CORE_FIXTURES: dict[str, Fixture] = {'after_plane': Fixture(path='misc/bench/corpus/after_plane', prepare=None, expect_visual_kinds=frozenset({'rect', 'svg_sprite'}), 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({'rect', 'svg_sprite'}), 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({'rect', 'svg_sprite'}), 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({'rect', 'svg_sprite'}), 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({'rect', 'svg_sprite'}), 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({'rect', 'svg_sprite'}), 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({'rect', 'path'}), 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 = 'misc/bench/corpus'#
Where the bench-owned fixtures live. NOT under
examples/, and the reason is mechanical rather than tidiness:.gitignoreexcludes everyexamples/*/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
preparestep, so their pixels are a function of the repo alone — wherepromote_demo’s are a function ofcutan.characters.promote, and would need re-blessing whenever that changes.
- exception an.bench.core_corpus.CaptureError[source]#
Bases:
RuntimeErrorA capture could not produce something the metrics need.
- an.bench.core_corpus.FILM_FRAMES_RELPATH: str = 'film_frames'#
Where an assembled scene’s composed frames are written, under the render’s work directory, by
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 = '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 = '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='')[source]#
Bases:
objectA corpus scene: where it lives, how to build it, what it must render.
- expect_visual_kinds: frozenset#
Visual kinds the staged scene MUST contain — see the module docstring.
- golden_frames: tuple[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 —
--blessrefuses a pair whose two frames are pixel-identical, which is not hypothetical:promote_demo’s frame 0 and itsduration/2frame differ by exactly zero pixels.
- an.bench.core_corpus.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.core_corpus.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.core_corpus.RENDER_WORK_RELPATH: 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)[source]#
The
scene_contract_sha256a render offixturewould 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_assetsas the bench sets it) in a throwaway copy, and hashes the documents. It is the default-leg twin ofcapture_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:
- an.bench.core_corpus.compose_film_frames(scene, work_dir)[source]#
An assembled scene’s composed frames, written under
work_dirfrom its shots’ frames (an.assemble.write_film_frames());Nonefor a scene that is a plain concatenation of its shots.
- an.bench.core_corpus.frames_dir_for(work_dir, shot_id)[source]#
Where a render left the frames of
shot_id(or of the composed film).- Return type:
>>> 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)[source]#
{frame key: (blessed sha256, today's sha256)}for every frame the committed bless record ofnamepins, read from a render’s work dir.Decoded pixels, never file bytes (
an.bench.golden’s criterion).
- an.bench.core_corpus.render_fixture(fixture, *, repo_root, base)[source]#
Render
fixturecold, the way the bench does, in a copy underbase.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:
- an.bench.core_corpus.stage_copy(fixture_dir, base)[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.- Return type: