annie.media.preview =================== .. py:module:: annie.media.preview .. autoapi-nested-parse:: Build Browse preview images for a video (service; needs the ``media`` extra). Produces the static thumbnail (clean first frame) and the annotated five-frame strip (vdet boxes in blue, the protagonist track in green) shown on each Browse row. Decoding is lazy via :mod:`annie.decode`, so importing this module never requires torch; call :func:`annie.decode.media_available` before using it. Attributes ---------- .. autoapisummary:: annie.media.preview.HIDPI_SCALE Functions --------- .. autoapisummary:: annie.media.preview.to_data_uri annie.media.preview.band_frame_indices annie.media.preview.build_preview annie.media.preview.build_grid_preview annie.media.preview.build_band_strip annie.media.preview.report_decode_failure Module Contents --------------- .. py:data:: HIDPI_SCALE :value: 2 .. py:function:: to_data_uri(image: PIL.Image.Image, box: tuple[int, int] | None = None) -> str Encode a PIL image as a self-contained ``data:`` WebP URI. Embedding the pixels in the element (rather than serving a per-client temp file) means a thumbnail/strip frame never produces an orphaned static route that 404s after a reconnect. The flip side is that every embedded frame is held in memory twice — server-side in the element's props, and again in the browser tab — for as long as its row is on the page. So the image is first downscaled to the box it will actually be displayed in (times :data:`HIDPI_SCALE`) and encoded as lossy WebP rather than lossless PNG, which cuts a Browse row's payload by roughly 15x. :param image: The image to encode. :param box: The ``(width, height)`` CSS size of the element that will show the image. The image is shrunk to fit ``HIDPI_SCALE`` times this, preserving aspect ratio. ``None`` encodes at full resolution. :returns: A ``data:image/webp;base64,…`` string usable as a ``ui.image`` source. .. py:function:: band_frame_indices(start_sec: float, end_sec: float, fps: float, num_frames: int, count: int = 5) -> list[int] Evenly sample ``count`` frame indices across the span ``[start_sec, end_sec)``. Converts the band's seconds to frame indices via ``fps`` and picks ``count`` positions spread across the span (inclusive of both ends when ``count > 1``), so a clip's preview strip shows that span rather than the whole video. Indices are clamped into ``[0, num_frames - 1]`` and a degenerate or reversed span collapses to the single start frame. :param start_sec: Span start, in seconds. :param end_sec: Span end, in seconds. :param fps: Frames per second of the source video. :param num_frames: Total frame count, used to clamp indices. :param count: Number of frames to sample. :returns: ``count`` (or fewer, when the span is a single frame) frame indices, ascending. .. py:function:: build_preview(entry: annie.core.models.VideoEntry, count: int = 5) -> tuple[PIL.Image.Image, list[PIL.Image.Image], int] Decode a video's strip and draw its annotations. :param entry: The video to preview; ``entry.video_path`` must be set. :param count: Number of strip frames (first / ¼ / ½ / ¾ / last by default). :returns: the clean first frame as a thumbnail, a list of annotated strip frames (vdet blue, active track green), and the video's total frame count. :rtype: A ``(thumbnail, strip, num_frames)`` triple :raises ValueError: If the entry has no video to decode. :raises annie.decode.MediaUnavailableError: If the ``media`` extra is absent. .. py:function:: build_grid_preview(entry: annie.core.models.VideoEntry) -> tuple[PIL.Image.Image, int] Decode a video's middle (½) frame with its annotations drawn on it. The Browse grid view shows a single static frame per video, so this is the fast counterpart to :func:`build_preview`: it decodes only three sample frames in one pass and keeps the middle one. ``"approximate"`` seek is safe here because the middle index — unlike the strip's last frame — never risks the tail overshoot that :func:`annie.media.decode.read_strip` guards against with ``"exact"`` mode. :param entry: The video to preview; ``entry.video_path`` must be set. :returns: the annotated middle frame (vdet blue, active track green) and the video's total frame count. :rtype: An ``(image, num_frames)`` pair :raises ValueError: If the entry has no video to decode. :raises annie.media.decode.MediaUnavailableError: If the ``media`` extra is absent. .. py:function:: build_band_strip(video_path: str | pathlib.Path, start_sec: float, end_sec: float, count: int = 5) -> list[PIL.Image.Image] Decode a plain strip spanning one band's ``[start_sec, end_sec)`` window. Used by the Segment-review task to preview a clip's span. Unlike :func:`build_preview` this draws no track/vdet overlay — the reviewer is judging the temporal cut, so the raw frames of the span are what matters — and it decodes only the sampled frames of that span rather than the whole video. :param video_path: The long video the clip belongs to. :param start_sec: Span start, in seconds. :param end_sec: Span end, in seconds. :param count: Number of frames to sample across the span. :returns: The sampled span frames as RGB images (empty if nothing could be decoded). :raises annie.media.decode.MediaUnavailableError: If the ``media`` extra is absent. .. py:function:: report_decode_failure(video: pathlib.Path | str | None, exc: BaseException) -> str Log a failed frame decode once, and return the reason to show on the frame box. Every Browse row and Annotator strip decodes on its own, so one broken decoder would otherwise fail silently dozens of times, or flood the Log tab. A decoder that cannot load at all is one problem for every video and is logged once per process; any other failure is logged once per video. :param video: The video that failed to decode. :param exc: The exception the decode raised. :returns: The short reason, for a tooltip on the broken-frame icon.