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