| """Artifacts: the pixel-space data structures that only PixelCursor may produce. |
| |
| External code may hold and inspect Artifacts but cannot construct them directly; |
| the only path to a fresh Artifact is a cursor write_* op or a loader in this |
| module. This is the structural enforcement of the "cursor owns the studio" claim. |
| """ |
| from __future__ import annotations |
| from dataclasses import dataclass, field |
| from typing import Union |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
| from ._internal import _MUTATION_TOKEN, _verify |
|
|
|
|
| @dataclass(frozen=True, slots=True, eq=False, repr=False) |
| class Image: |
| pixels: np.ndarray |
| name: str = "" |
| _token: object = field(default=None, repr=False) |
|
|
| def __post_init__(self) -> None: |
| _verify(self._token) |
|
|
| def __repr__(self) -> str: |
| return f"Image(shape={tuple(self.pixels.shape)}, name={self.name!r})" |
|
|
|
|
| @dataclass(frozen=True, slots=True, eq=False, repr=False) |
| class FrameStack: |
| frames: np.ndarray |
| fps: int = 24 |
| name: str = "" |
| _token: object = field(default=None, repr=False) |
|
|
| def __post_init__(self) -> None: |
| _verify(self._token) |
|
|
| @property |
| def num_frames(self) -> int: |
| return int(self.frames.shape[0]) |
|
|
| def __repr__(self) -> str: |
| return f"FrameStack(shape={tuple(self.frames.shape)}, fps={self.fps}, name={self.name!r})" |
|
|
|
|
| Artifact = Union[Image, FrameStack] |
|
|
|
|
| def _new_image(pixels: np.ndarray, name: str = "") -> Image: |
| return Image(pixels=pixels, name=name, _token=_MUTATION_TOKEN) |
|
|
|
|
| def _new_framestack(frames: np.ndarray, fps: int = 24, name: str = "") -> FrameStack: |
| return FrameStack(frames=frames, fps=fps, name=name, _token=_MUTATION_TOKEN) |
|
|
|
|
| def load_image(path: str | Path) -> Image: |
| from PIL import Image as PILImage |
|
|
| arr = np.array(PILImage.open(path).convert("RGB")) |
| return _new_image(arr, name=str(path)) |
|
|