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– the frame directory’s naming (frame_%06d.png).an.media.supersample– the SPATIAL resolve: an exactk x kblock mean.an.media.shutter– the TEMPORAL resolve: a frame’s sample instants averaged.an.media.mp4– the MP4 sink with the pinned argv, plus the shot’s audio mux.an.media.gif– the GIF sink’s one palette recipe.an.media.sinks– theFrameSinkprotocol 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.
>>> from an.media import get_sink
>>> get_sink("mp4").suffix
'.mp4'
Functions
|
|
|
Validate a supersample factor, or refuse with the reason. |
|
Validate |
|
Where frame |
|
A sink by name, built with |
|
Spatially resolve each screenshot by |
|
The frame indices in |
|
Register a sink factory under |
The registered sink names. |
|
|
|
Classes
|
Writes a frame directory as one deliverable. |
|
The demo gallery's palette recipe. |
|
The silent picture mux (the shot's audio is laid by |
|
The frames themselves, renumbered from 0 into |
Exceptions
A sink could not write what it was asked to. |
- class an.media.FrameSink(*args, **kwargs)[source]#
Bases:
ProtocolWrites a frame directory as one deliverable.
- class an.media.GifSink(crop='', fps=12, width=480, max_colours=128, name='gif', suffix='.gif')[source]#
Bases:
objectThe demo gallery’s palette recipe.
- exception an.media.MediaError[source]#
Bases:
RuntimeErrorA 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 withCutoutRenderError.
- class an.media.Mp4Sink(pix_fmt=None, name='mp4', suffix='.mp4')[source]#
Bases:
objectThe silent picture mux (the shot’s audio is laid by
an.media.mp4.mux_shot()).pix_fmt=Noneis the module default AT CALL TIME, the seam the bench pulls.
- class an.media.PngSequenceSink(name='png', suffix='')[source]#
Bases:
objectThe frames themselves, renumbered from 0 into
output(a directory).>>> 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)[source]#
(H*k, W*k, C)uint8 ->(H, W, C)uint8, by an exactk x kmean.Two-step, summing rows and then columns in
uint16, rather than the obviousreshape(...).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.rintis banker’s rounding, so a block averaging exactly.5goes 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:
>>> 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)[source]#
Validate a supersample factor, or refuse with the reason.
- Return type:
>>> 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)[source]#
Validate
frame_samplesagainst 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.>>> 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.get_sink(name, **knobs)[source]#
A sink by name, built with
knobs; the error lists what exists.- Return type:
- an.media.mean_png_bytes(shots, *, factor)[source]#
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:
- an.media.missing_frames(frames_dir, total_frames)[source]#
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.
>>> 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)[source]#
Register a sink factory under
name; refuse a silent replacement.- Return type:
- an.media.temporal_mean(frames)[source]#
kequal-shape uint8 frames -> their exact per-pixel mean, uint8.Rounded half-to-even, the rule
block_mean_resolvespells 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:
>>> 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
The frame directory every engine writes and every sink reads. |
|
The GIF sink: ONE palette recipe, for flat 2D art. |
|
Tile images into one contact sheet: |
|
The MP4 sink: PNG frames -> the delivered H.264 mp4, with the pinned argv. |
|
The temporal half of the frame stage: average several instants into one frame. |
|
Frame sinks: a frame directory in, one deliverable out -- one sink per format. |
|
Render bigger, then resolve back exactly — the supersample knob's two halves. |