# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. # SPDX-License-Identifier: Apache-2.0 """SimReady Central's library media rules for RTX stills, spins (+ HD frames) and RTX clips (+ HD), vendored. pipeline/library_media.py checks the RTX media an add brings (see its module doc, "Media adds") with exactly the rules SimReady Central's writers and readers share: this file is a VERBATIM subset of Central's ``simready-api/libmedia_rules.py`` at b777ed103f785b4386f43a52998eb1fc86d5aa93 (2026-10-09, its last change on main as of 2026-10-16) — its source lines 59-60, 66-158, 227-232, 270-356, 358-479, 524-731, 787-886, 888-924 and 944-1427, in order, nothing else changed: the constants of those kinds, ``drop_v`` (every ``v`` is one), the still's, spin's, sheets', crop's, spin HD's and RTX clip's problem / v / block functions, the size readers (jpeg_size, webp_size, mp4_size) and the self-tests' fake files. Plus, in its place between those ranges, ``user_joints`` as SimReady Central !733 (4a0b26cff51c2d10a4cbfa6c1b88712efa315e37, 2026-10-16, source lines 189-197) writes it: the moving-joint rule every count of an asset shares (the manifest's ``moving``, the Explorer's joints gate and panel); !733 changes no other line this file vendors. Do not edit it here: re-vendor the same ranges from a newer Central commit (tests/test_library_media.py compares it with Central's file when CENTRAL_LIBMEDIA_RULES names one: !733's, or main's once !733 is merged). Standard library only. """ from __future__ import annotations import hashlib import json import math import re import struct from typing import Optional CLIP_NAMES = ("jointsclip.mp4", "jointsclip_colliders.mp4", "poster.jpg", "jointsclip_card.mp4") CLIP_ROLES = {"textured": CLIP_NAMES[0], "colliders": CLIP_NAMES[1], "poster": CLIP_NAMES[2], "card": CLIP_NAMES[3]} # An RTX still (stills/finalize.py): the tile (640 x 640) every thumbnail surface takes and the poster (1280 x 800) the # details panel's Image tab takes, in the role order its ``v`` hashes them (finalize's still.json ``v``). STILL_ROLES = {"poster": "still_poster.jpg", "tile": "still_tile.jpg"} # routers/library_media.py STILL_ROLES STILL_META = "still.json" STILL_META_KEYS = ("renderer", "look", "spp", "rendered_from") # the block's meta, in this order STILL_CAP = 8 << 20 # each file: the router's asset LRU admits it whole # An RTX spin (a turntable: the asset turns under a fixed camera and light; stills/finalize.py --kind spin): frames # spin_000.webp … in frame-index order, which its ``v`` hashes (finalize's spin.json ``v``). Its poster is the frame it # opens on: ``start`` (spin contract v1.1: the representative frame finalize picked, ``start_reason`` saying why), else # frame 0. SPIN_PATTERN = "spin_{k:03d}.webp" # routers/library_media.py SPIN_PATTERN: the one pattern it serves SPIN_NAME_RE = re.compile(r"spin_([0-9]{3})\.webp") SPIN_META = "spin.json" SPIN_FORMAT = "webp" SPIN_FRAMES = (8, 120) # genlab-rtx's frame range (spin contract §1) SPIN_MAX_SIDE = 2048 # genlab-rtx's cap on a side SPIN_FRAME_CAP = 512 << 10 # each frame (~40–60 KB at 768² q85); the asset LRU admits it whole SPIN_NAMED_AZ = {"front": 0, "right": 270, "back": 180, "left": 90} # the generator's azimuths, in this order SPIN_START_REASON_MAX = 200 # a short line: why the spin opens on its start frame # The sheets layout (spin contract §2.1, v1.2; finalize's spin_sheets.json): the frames packed into four WebP sheets — # sheet j holds the frames k with k mod 4 = j in order, frame k in cell i = k div 4 at row i div cols, col i mod cols # of a cols x rows grid of ``size`` cells — and the start frame alone (spin_start.webp, the poster), so a publish # sends 5 objects a spin, not 72. A block carries this layout OR the frame pattern, never both; its ``v`` hashes the # sheets in order, then the start file. SPIN_SHEETS = tuple(f"spin_s{j}.webp" for j in range(4)) # routers/library_media.py SPIN_SHEETS SPIN_SHEET_OF = "k mod 4" SPIN_START_FILE = "spin_start.webp" SPIN_SHEETS_META = "spin_sheets.json" SPIN_SHEET_CAP = 6 << 20 # each sheet (18 frames at 768² q85 ≈ 0.3–1 MB); the asset LRU admits it SPIN_SHEET_MAX_PX = 1 << 24 # a sheet's pixels: iOS's 16.7 MP decoded-image limit (finalize's) SPIN_SHEET_MAX_SIDE = 16383 # WebP's largest side # A spin's optional CROP (spin contract §2.2, v1.3): two boxes [x, y, w, h], normalized 0–1 of the frame (origin top # left, 4 decimals), the same for every frame and sheet cell — ``object`` the union over all frames of the asset's # pixels (its floor shadow too), ``square`` the smallest square holding it grown 4 % a side, shifted into the frame. # The Explorer's square tiles show the start frame and the hover turn under ``square``. Neither the files nor ``v`` # change with it: a crop alone is a manifest-only change (publish.sh sends a spin by ``v`` and layout). SPIN_CROP_BOXES = ("object", "square") # v1.4 (§2.2.1), optional, the three together: ``start`` the square that fits the START frame's own object at ``fill`` # (the tile), ``frames`` one square a frame (frames[start] = start, never smaller than it: the hover eases out only as # far as the turn needs and back), ``fill`` the object's longer side over its square's side (0.92). The 72 squares # make a spin's block about 2.3 KB longer: see LANE_MANIFEST_MAX_BYTES. SPIN_CROP_V14 = ("start", "frames", "fill") # How the crop was made (optional, beside ``crop``): "geometry" — rtx/spin/sw_geo_crop.py's boxes of the asset's own # hull projected through the spin's camera, written by it as spin.json's ``meta.crop_source`` (the RTX lane's spins, # the batch's 960 ones); absent — finalize's pixel rule (v1.4, calibrated on look A). A later pixel-rule fix re-crops # the spins without it, manifest-only, and leaves these. SPIN_CROP_SOURCES = ("geometry",) # v1.5 (load speed, 2026-10-09): the per-frame squares out of the manifest. A crop's ``frames`` (a spin's, v1.4; an RTX # clip's, one per clip frame: together ~57 % of simready-prod's manifest) may live in a side file instead: # ``frames_file`` the kind's one name (CROP_FILES), ``frames_v`` sha256[:12] of its bytes, where ``frames`` was; never # both. The body is {"frames": [[x, y, w, h], …]}, the very array (crop_file_bytes: compact JSON), stored beside the # block's files (a spin's under spin//, an RTX clip's under clips//) and served at # /library_media///?v=, immutable under it. Neither the block's files nor its ``v`` change: # a crop is manifest-only since v1.3. CROP_FILE_KEYS = ("frames_file", "frames_v") SPIN_CROP_FILE = "spin_crop.json" # routers/library_media.py CROP_FILES: the one name each kind serves RTX_CLIP_CROP_FILE = "jointsclip_rtx_crop.json" CROP_FILES = {"spin": SPIN_CROP_FILE, "rtxclip": RTX_CLIP_CROP_FILE} CROP_FILE_CAP = 256 << 10 # an RTX clip's 600 squares are ~18 KB compact; the asset LRU admits it # The library-ingest lane's manifest cap: the default of libingest_config.MAX_MANIFEST_BYTES (the chart's # simreadyApi.libraryIngest.maxManifestBytes and compose's LIBRARY_INGEST_MAX_MANIFEST_BYTES say the same). A lane # commit that changes a manifest is refused (409 manifest_full) when the manifest's compact JSON (manifest_bytes, the # bytes the lane writes) is longer: the cap counts uncompressed bytes. Sized on staging's manifests (2026-10-07): # simready-prod is 5.30 MB (2,653 spins, none with a crop). It grows to ~11.3 MB with a v1.4 crop on every spin and to # ~13.0 MB with an RTX showcase (98 squares) on each of its 485 joints clips; simready-stg grows to ~8.4 MB and # hf-srp-staging-controlled to ~6.2 MB. The old 6,000,000 cap would have refused all three. build.py and publish.sh # check a bundle against it (manifest_size_problem). LANE_MANIFEST_MAX_BYTES = 24_000_000 # A spin's optional hover VIDEO (the Explorer's tile hover plays it instead of turning the frames; LibraryHoverClip): # ``files.card`` = SPIN_CARD beside either layout's files — the turn as one looping MP4 whose frame 0 is the start # frame — and ``card_fps`` its frame rate (SPIN_CARD_FPS when spin.json gives none). ``v`` then hashes the card last # (after the frames, or the sheets and spin_start.webp), so it is immutable under ``v`` like the rest, and publish.sh # --spins sends it with them. A block without ``files.card`` carries no ``card_fps``. SPIN_CARD = "spin_card.mp4" # routers/library_media.py SPIN_CARD: the one name it serves SPIN_CARD_FPS = 24 SPIN_CARD_FPS_RANGE = (1, 120) SPIN_CARD_CAP = 4 << 20 # a clip's cap (the generator's); the router's clip LRU admits it whole SPIN_CROP_SLACK = 1e-4 # x + w may reach 1.0001: the 4-decimal rounding SPIN_CROP_SQUARE_TOL = 1e-3 # a square's w and h, each rounded on its own # A spin's optional HD frames (spin contract §2.3, ``spin.hd``; the expanded RTX view loads them, nothing else): the # parent spin's frames at more pixels — HD frame k is the parent's frame k for every k, the same camera — one WebP a # frame (never sheets), spin_hd_000.webp … under library//spinhd//. The block sits INSIDE the spin block # (either layout) and holds only what is its own, SPIN_HD_KEYS: ``v`` drop_v over its frames in frame order; ``of`` # the parent's FRAMES v (spin_v over spin_000 … with no card — never the block's ``v``, which a sheets layout's start # file or a hover card changes without changing a frame); ``size`` [s, s], the parent square and s above its side, at # most SPIN_MAX_SIDE (2048; the library renders 1536, the lead's pick of 2026-10-07); ``format`` webp; ``files`` the one # pattern; ``bytes`` the frames' sum (the Explorer's budget). Frames, step, az0, elevation, start, named, crop and # meta are the parent's. The Explorer's reader (libraryMedia.readSpinHd) reads v, size, format and files.pattern. SPIN_HD_PATTERN = "spin_hd_{k:03d}.webp" # routers/library_media.py SPIN_HD_PATTERN: the one pattern it serves SPIN_HD_NAME_RE = re.compile(r"spin_hd_([0-9]{3})\.webp") SPIN_HD_META = "spin_hd.json" # finalize --kind spin-hd: the block (``of`` included) beside the frames SPIN_HD_KEYS = ("v", "of", "size", "format", "files", "bytes") # the block, in this order SPIN_HD_FRAME_CAP = 512 << 10 # each frame: 13–143 KB measured on 77 real 1536 renders (2026-10-08) def user_joints(doc: dict) -> int: """The joints a user moves (the viewer's sliders): prismatic / revolute, never a mimic or coupled follower nor a closed loop's solved or closing joint (``loop`` / ``closes``: the Explorer's isUserJoint). ValueError when ``joints`` is there but no list of objects.""" joints = doc.get("joints", []) if not isinstance(joints, list) or not all(isinstance(j, dict) for j in joints): raise ValueError("articulation.json joints is not a list of objects") return sum(1 for j in joints if j.get("type") in ("prismatic", "revolute") and not j.get("mimic") and not j.get("coupled") and j.get("loop") is None and not j.get("closes")) def drop_v(present: list[tuple[str, bytes]]) -> str: """The drop's ``v`` over ``[(name, bytes), …]`` in role order (publish.sh --drops checks the same hash).""" h = hashlib.sha256() for name, data in present: h.update(name.encode() + b"\0" + data) return h.hexdigest()[:12] def still_v(present: dict[str, bytes]) -> str: """The still's ``v``: drop_v over its present files in STILL_ROLES order (any other name is ignored).""" return drop_v([(n, present[n]) for n in STILL_ROLES.values() if n in present]) _JPEG_SOF = frozenset((0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF)) def jpeg_size(data: bytes) -> Optional[tuple[int, int]]: """A JPEG's (width, height) from its first SOF marker; None when ``data`` is no JPEG (or a truncated one).""" if data[:3] != b"\xff\xd8\xff": return None i = 2 while i + 4 <= len(data): if data[i] != 0xFF: return None marker = data[i + 1] if marker == 0xFF: # fill byte i += 1 continue if marker in (0xD8, 0x01) or 0xD0 <= marker <= 0xD7: # no length i += 2 continue length = int.from_bytes(data[i + 2:i + 4], "big") if length < 2: return None if marker in _JPEG_SOF: if i + 9 > len(data): return None h, w = int.from_bytes(data[i + 5:i + 7], "big"), int.from_bytes(data[i + 7:i + 9], "big") return (w, h) if w and h else None if marker == 0xDA: # scan data before any SOF: not a JPEG we read return None i += 2 + length return None def still_problem(present: dict[str, bytes], meta) -> Optional[str]: """Why a finalized still cannot be a manifest's ``still``: both files (STILL_ROLES), each a non-empty JPEG of at most STILL_CAP bytes, the tile square; ``meta`` (the block's own: STILL_META_KEYS) a renderer, a look, a positive ``spp`` and a ``rendered_from`` path.""" for name in STILL_ROLES.values(): data = present.get(name) if not data: return f"no {name}" if data is None else f"{name} is empty" if len(data) > STILL_CAP: return f"{name} is {len(data)} bytes (> {STILL_CAP})" if jpeg_size(data) is None: return f"{name} is no JPEG" w, h = jpeg_size(present[STILL_ROLES["tile"]]) if w != h: return f"{STILL_ROLES['tile']} is {w}x{h}: not square" return render_meta_problem(meta, "still") def render_meta_problem(meta, what: str) -> Optional[str]: """Why an RTX render's block ``meta`` (STILL_META_KEYS: a still's, a spin's) cannot be one: a renderer, a look, a positive ``spp`` and a ``rendered_from`` path below the content root. ``what`` names the kind in the reason.""" if not isinstance(meta, dict): return f"the {what}'s meta is not an object" for key in ("renderer", "look", "rendered_from"): if not (isinstance(meta.get(key), str) and meta[key].strip()): return f"the {what}'s meta has no {key}" spp = meta.get("spp") if not (isinstance(spp, int) and not isinstance(spp, bool) and spp > 0): return f"the {what}'s meta has no spp (a positive count)" if meta["rendered_from"].startswith("/") or "://" in meta["rendered_from"]: return f"the {what}'s rendered_from is not a path below the content root" return None def still_block(present: dict[str, bytes], meta: dict) -> dict: """The entry's ``still``: ``{v, files: {role: name} (STILL_ROLES order), meta: {renderer, look, spp, rendered_from}}``. No ``src``: a still is shown whatever the hit's hash (an Isaac re-publish leaves the visuals as they were). ValueError (why) when it cannot be one (still_problem).""" meta = {k: meta[k] for k in STILL_META_KEYS if isinstance(meta, dict) and k in meta} if why := still_problem(present, meta): raise ValueError(why) return {"v": still_v(present), "files": dict(STILL_ROLES), "meta": meta} def fake_jpeg(width: int, height: int, tag: bytes = b"") -> bytes: """A minimal JPEG header jpeg_size reads (SOI, a comment carrying ``tag``, SOF0, EOI): the self-tests' stills.""" com = b"\xff\xfe" + (len(tag) + 2).to_bytes(2, "big") + tag sof = b"\xff\xc0\x00\x11\x08" + height.to_bytes(2, "big") + width.to_bytes(2, "big") + b"\x03" + bytes(9) return b"\xff\xd8" + com + sof + b"\xff\xd9" def spin_name(k: int) -> str: """Frame ``k``'s file name (SPIN_PATTERN).""" return SPIN_PATTERN.format(k=k) def spin_index(name: str) -> Optional[int]: """The frame index a file name carries (SPIN_NAME_RE, three digits), None for any other name.""" m = SPIN_NAME_RE.fullmatch(name) return int(m.group(1)) if m else None def spin_files(start: int = 0) -> dict: """A spin block's ``files``: the one pattern, and the poster = the frame it opens on (``start``, else frame 0).""" return {"pattern": SPIN_PATTERN, "poster": spin_name(start)} def spin_v(present: dict[str, bytes], card: bool = False) -> str: """The spin's ``v``: drop_v over its frame files in frame-index order (any other name is ignored), then — ``card``: its block lists the hover video — spin_card.mp4.""" frames = sorted((k, n) for n in present if (k := spin_index(n)) is not None) return drop_v([(n, present[n]) for _k, n in frames] + ([(SPIN_CARD, present[SPIN_CARD])] if card else [])) def spin_sheet_files(grid, cell) -> dict: """A sheets-layout block's ``files`` (spin contract §2.1): the four sheets, the grid [cols, rows], the cell [w, h] (the frames' size), the rule, and the poster = the start frame's own file.""" return {"sheets": list(SPIN_SHEETS), "grid": [int(grid[0]), int(grid[1])], "cell": [int(cell[0]), int(cell[1])], "sheet_of": SPIN_SHEET_OF, "poster": SPIN_START_FILE} def spin_sheet_cell(k: int, cols: int) -> tuple[int, int, int]: """(sheet, row, col) of frame k: sheet k mod 4, cell i = k div 4 at row i div cols, col i mod cols.""" i = k // len(SPIN_SHEETS) return k % len(SPIN_SHEETS), i // cols, i % cols def spin_sheets_v(present: dict[str, bytes], card: bool = False) -> str: """The sheets layout's ``v``: drop_v over the sheets in order, then spin_start.webp (finalize's spin_sheets.json), then — ``card``: its block lists the hover video — spin_card.mp4.""" return drop_v([(n, present[n]) for n in (*SPIN_SHEETS, SPIN_START_FILE, *((SPIN_CARD,) if card else ()))]) def spin_layout(files) -> Optional[str]: """A block's layout from its ``files``: "frames" (``pattern``), "sheets" (``sheets``), None for both or neither.""" if not isinstance(files, dict) or ("pattern" in files) == ("sheets" in files): return None return "frames" if "pattern" in files else "sheets" def spin_card_listed(files) -> bool: """Whether a spin block's ``files`` list its hover video (``card``; spin_card_problem says whether it is one).""" return isinstance(files, dict) and "card" in files def spin_layout_files(files): """A spin block's ``files`` less its hover video's ``card``: what its layout's own rule compares.""" return {k: x for k, x in files.items() if k != "card"} if isinstance(files, dict) else files def spin_files_of(block: dict) -> list[str]: """The files a spin block lists, in ``v`` order: its frames (a pattern) or its sheets then spin_start.webp, then spin_card.mp4 where it lists its hover video.""" card = [SPIN_CARD] if spin_card_listed(block.get("files")) else [] if spin_layout(block.get("files")) == "sheets": return [*SPIN_SHEETS, SPIN_START_FILE, *card] return [spin_name(k) for k in range(block["frames"])] + card def mp4_ok(data: bytes) -> bool: """Whether ``data`` opens as an MP4 (an ISO BMFF ``ftyp`` box first).""" return len(data) >= 12 and data[4:8] == b"ftyp" def spin_card_problem(present: Optional[dict], info: dict) -> Optional[str]: """Why a spin's hover video cannot be one: ``files.card`` (when given) SPIN_CARD, ``card_fps`` (when given, only beside a card) a frame rate in SPIN_CARD_FPS_RANGE, ``present`` spin_card.mp4 a non-empty MP4 of at most SPIN_CARD_CAP bytes (``present`` None: the manifest's half alone). No ``files.card``: nothing to check (a card file beside it is ignored, as finalize's extra files are).""" files, fps = info.get("files"), info.get("card_fps") if not spin_card_listed(files): return "card_fps without a card (files.card)" if fps is not None else None if files["card"] != SPIN_CARD: return f"files.card {files['card']!r} is not {SPIN_CARD}" lo, hi = SPIN_CARD_FPS_RANGE if fps is not None and not (_number(fps) and lo <= fps <= hi): return f"card_fps {fps!r} is not a frame rate in {lo}–{hi}" if present is None: return None data = present.get(SPIN_CARD) if not data: return f"no {SPIN_CARD}" if data is None else f"{SPIN_CARD} is empty" if len(data) > SPIN_CARD_CAP: return f"{SPIN_CARD} is {len(data)} bytes (> {SPIN_CARD_CAP})" if not mp4_ok(data): return f"{SPIN_CARD} is no MP4" return None RTX_CLIP_ROLES = {"textured": "jointsclip_rtx.mp4", "card": "jointsclip_rtx_card.mp4", "poster": "jointsclip_rtx_poster.webp"} # routers/library_media.py RTX_CLIP_FILES RTX_CLIP_CAP = 8 << 20 # each video (≤ 1.5 MB encoded); the router's clip LRU admits it whole RTX_CLIP_POSTER_CAP = 512 << 10 RTX_CLIP_DEVICE = "lab-rtx" # meta.render_device of every RTX clip (the Explorer's RTX label) RTX_CLIP_FPS = (1, 120) RTX_CLIP_FRAMES = (1, 600) # genlab-rtx joints mode's frame range RTX_CLIP_FILL = (0.5, 1.0) RTX_CLIP_KEYS = ("v", "files", "size", "crop", "meta") # the block (rtx_clip_block), in this order; + its ``hd`` RTX_CLIP_CROP_KEYS = (*SPIN_CROP_BOXES, "start", "frames", "fill", "card_from") # finalize_w2's (card_from optional) # An RTX clip's optional HD tier (``clip.rtx.hd`` / ``rtxclip.hd``; the expanded Clip view loads it, nothing else): the # parent clip's render at more pixels — the same job with only its width and height changed (camera, crop fractions, # timeline, look, spp) — encoded as the textured clip is, one MP4, jointsclip_rtx_hd.mp4 under # library//rtxcliphd//. The block sits INSIDE the RTX clip block and holds only what is its own, # RTX_CLIP_HD_KEYS: ``v`` drop_v over its one file; ``of`` the parent block's ``v`` (rtx_clip_v over the 768 files: a # re-rendered or re-cut clip leaves the hd off it); ``size`` [s, s], the parent's square frame and s above its side, at # most SPIN_MAX_SIDE; ``files`` {"textured": RTX_CLIP_HD_NAME}; ``bytes`` its size (the Explorer's budget), at most # RTX_CLIP_HD_CAP. Crop, start, card, timeline and meta are the parent's (the card stays the 768 one). The Explorer's # reader (libraryMedia.readRtxClip's ``hd``) reads v, size, files.textured and bytes. RTX_CLIP_HD_NAME = "jointsclip_rtx_hd.mp4" # routers/library_media.py RTX_CLIP_HD_FILES: the one name it serves RTX_CLIP_HD_ROLES = {"textured": RTX_CLIP_HD_NAME} RTX_CLIP_HD_KEYS = ("v", "of", "size", "files", "bytes") # the block, in this order RTX_CLIP_HD_CAP = 24 << 20 # the lead's cap (2026-10-08); 0.25-1.6 MB measured at 1440 (CRF 20) RTX_CLIP_HD_META = "rtxclip_hd.json" # stills/clip_hd.py's: the block (``of`` included) beside the file def rtx_clip_v(present: dict[str, bytes]) -> str: """An RTX clip's ``v``: drop_v over its present files in RTX_CLIP_ROLES order (any other name is ignored).""" return drop_v([(n, present[n]) for n in RTX_CLIP_ROLES.values() if n in present]) def _listed(present: dict[str, bytes], files: dict) -> dict[str, bytes]: """The present files a block lists (``files``: {role: name}): what its ``v`` hashes and what a publish sends.""" return {n: present[n] for n in files.values() if n in present} def mp4_size(data: bytes) -> Optional[tuple[int, int]]: """An MP4's frame (width, height): its first track header (``moov`` → ``trak`` → ``tkhd``, either version) with a non-zero size (an audio track's is 0 x 0), its 16.16 fixed-point numbers whole. None when ``data`` is no MP4 or carries no such track (the ``moov`` box may come after ``mdat``: the whole file is read).""" def boxes(start: int, end: int): at = start while at + 8 <= end: size, kind = struct.unpack_from(">I4s", data, at) head = 8 if size == 1: if at + 16 > end: return size, head = struct.unpack_from(">Q", data, at + 8)[0], 16 elif size == 0: size = end - at if size < head or at + size > end: return yield kind, at + head, at + size at += size if not mp4_ok(data): return None for kind, body, end in boxes(0, len(data)): if kind != b"moov": continue for trak, t_body, t_end in boxes(body, end): if trak != b"trak": continue for tk, k_body, k_end in boxes(t_body, t_end): if tk != b"tkhd" or k_end - k_body < 4: continue at = k_body + (88 if data[k_body] == 1 else 76) # version 1: 64-bit times and duration if at + 8 > k_end: break w, h = (x >> 16 for x in struct.unpack_from(">II", data, at)) if w and h: return w, h break return None def fake_mp4(width: int, height: int, tag: bytes = b"") -> bytes: """A minimal MP4 mp4_size reads (``ftyp``, a ``moov`` with one ``trak`` whose version-0 ``tkhd`` carries the size, a ``free`` box carrying ``tag``): the self-tests' RTX clips.""" tkhd = bytes(76) + struct.pack(">II", width << 16, height << 16) box = lambda kind, body: struct.pack(">I", 8 + len(body)) + kind + body return box(b"ftyp", b"isom" + bytes(4) + b"isomiso2") + box(b"moov", box(b"trak", box(b"tkhd", tkhd))) + \ box(b"free", tag) def _square_box(box, name: str) -> Optional[str]: if not (isinstance(box, list) and len(box) == 4 and all(_number(n) for n in box)): return f"{name} {box!r} is not [x, y, w, h]" x, y, w, h = box if x < -SPIN_CROP_SLACK or y < -SPIN_CROP_SLACK or w <= 0 or h <= 0 or x + w > 1 + SPIN_CROP_SLACK \ or y + h > 1 + SPIN_CROP_SLACK: return f"{name} {box!r} is not a box inside the frame (0–1)" if abs(w - h) > SPIN_CROP_SQUARE_TOL: return f"{name} {box!r} is not square" return None def _same_box(a, b) -> bool: return all(abs(x - y) <= SPIN_CROP_SLACK for x, y in zip(a, b)) def rtx_clip_problem(block, present: Optional[dict] = None, spin: Optional[dict] = None) -> Optional[str]: """Why a ``clip.rtx`` block (the RTX moving-parts showcase; also read as a top-level ``rtxclip``) cannot be one: - its keys: RTX_CLIP_KEYS and an optional ``hd``, nothing rtx_clip_block never writes; - ``files``: exactly the roles of RTX_CLIP_ROLES with their fixed names (textured and card required, poster optional); ``present`` (when given) holds them non-empty: the videos MP4s ≤ RTX_CLIP_CAP whose track is a full frame of ``size`` (both are whole frames: the Explorer crops them), the poster a WebP ≤ RTX_CLIP_POSTER_CAP of ``size``; then ``v`` must be rtx_clip_v of the files it lists; - ``size``: a square frame [n, n] (the crop's boxes are normalized: a square box is a pixel square only there); - ``meta``: ``renderer`` "ovrtx …", ``render_device`` RTX_CLIP_DEVICE, a ``look``, ``fps`` and ``frames`` in range, ``width`` / ``height`` = ``size``; - ``crop``: ``object`` and ``square`` as a spin's crop (spin_crop_problem's boxes: what the Explorer's readCrop frames the hover and the Clip tab by), ``start`` and every one of ``frames`` (one per clip frame) a square inside the frame, ``frames[0]`` = ``start`` (the clip opens on the tile's own region), ``fill`` in RTX_CLIP_FILL, ``card_from`` a frame index; no key but RTX_CLIP_CROP_KEYS (v1.5: ``frames_file`` jointsclip_rtx_crop.json and ``frames_v`` in ``frames``' place, crop_ref_problem — the squares checked with the file, rtx_clip_crop_file_problem, here too where ``present`` holds it); with the entry's ``spin`` (when given): ``crop.start`` = ``spin.crop.start`` where the spin's crop has one (v1.4: the start frame's own fit, the tile), else ``spin.crop.square`` where the spin has a crop (the region a square Search tile shows of the spin's start frame, which is the clip's frame 0), and ``meta.spin.start`` (when given) = ``spin.start``. An ``hd`` it carries: rtx_clip_hd_problem's manifest half (``present`` never holds its file).""" if not isinstance(block, dict): return "clip.rtx is not an object" if extra := sorted(set(block) - set(RTX_CLIP_KEYS) - {"hd"}): return f"clip.rtx has keys rtx_clip_block never writes: {extra}" files = block.get("files") if not isinstance(files, dict) or not files or any(RTX_CLIP_ROLES.get(r) != n for r, n in files.items()): return f"clip.rtx.files {files!r} is not {{role: its fixed name}} of {RTX_CLIP_ROLES}" if "textured" not in files or "card" not in files: return "clip.rtx.files needs textured and card" if not (isinstance(block.get("v"), str) and re.fullmatch(r"[0-9a-f]{12}", block["v"])): return f"clip.rtx.v {block.get('v')!r} is not 12 hex" meta = block.get("meta") if not isinstance(meta, dict): return "clip.rtx.meta is not an object" if not (isinstance(meta.get("renderer"), str) and meta["renderer"].startswith("ovrtx ")): return f"clip.rtx.meta.renderer {meta.get('renderer')!r} is not an ovrtx render" if meta.get("render_device") != RTX_CLIP_DEVICE: return f"clip.rtx.meta.render_device {meta.get('render_device')!r} is not {RTX_CLIP_DEVICE!r}" if not (isinstance(meta.get("look"), str) and meta["look"].strip()): return "clip.rtx.meta has no look" lo, hi = RTX_CLIP_FPS if not (_number(meta.get("fps")) and lo <= meta["fps"] <= hi): return f"clip.rtx.meta.fps {meta.get('fps')!r} is not in {lo}–{hi}" n = meta.get("frames") lo, hi = RTX_CLIP_FRAMES if not (isinstance(n, int) and not isinstance(n, bool) and lo <= n <= hi): return f"clip.rtx.meta.frames {n!r} is not a frame count in {lo}–{hi}" size = block.get("size") if not (isinstance(size, list) and len(size) == 2 and all( isinstance(x, int) and not isinstance(x, bool) and 0 < x <= SPIN_MAX_SIDE for x in size)): return f"clip.rtx.size {size!r} is not [w, h]" if size[0] != size[1]: return f"clip.rtx.size {size} is not a square frame (its crop's square boxes are pixel squares only there)" if [meta.get("width"), meta.get("height")] != size: return f"clip.rtx.meta width x height {meta.get('width')} x {meta.get('height')} is not size {size}" crop = block.get("crop") if not isinstance(crop, dict): return "clip.rtx.crop is not an object" if extra := sorted(set(crop) - set(RTX_CLIP_CROP_KEYS) - set(CROP_FILE_KEYS)): return f"clip.rtx.crop has keys finalize never writes: {extra}" if why := spin_crop_problem({b: crop.get(b) for b in SPIN_CROP_BOXES}): # start, frames … beside them return f"clip.rtx.{why}" if why := _square_box(crop.get("start"), "clip.rtx.crop.start"): return why if crop_moved(crop): # v1.5: the squares in jointsclip_rtx_crop.json if why := crop_ref_problem(crop, RTX_CLIP_CROP_FILE, "clip.rtx.crop"): return why else: frames = crop.get("frames") if not (isinstance(frames, list) and len(frames) == n): return f"clip.rtx.crop.frames has {len(frames) if isinstance(frames, list) else 'no'} boxes, not {n}" for k, box in enumerate(frames): if why := _square_box(box, f"clip.rtx.crop.frames[{k}]"): return why if not _same_box(frames[0], crop["start"]): return f"clip.rtx.crop.frames[0] {frames[0]} is not crop.start {crop['start']}" lo, hi = RTX_CLIP_FILL if not (_number(crop.get("fill")) and lo < crop["fill"] <= hi): return f"clip.rtx.crop.fill {crop.get('fill')!r} is not in ({lo}, {hi}]" cf = crop.get("card_from", 0) if not (isinstance(cf, int) and not isinstance(cf, bool) and 0 <= cf < n): return f"clip.rtx.crop.card_from {cf!r} is not a frame index" if isinstance(spin, dict): sc = spin.get("crop") which = "start" if isinstance(sc, dict) and sc.get("start") is not None else "square" # v1.4: the start fit tile = sc.get(which) if isinstance(sc, dict) else None if isinstance(tile, list) and len(tile) == 4 and all(_number(x) for x in tile) \ and not _same_box(crop["start"], tile): return f"clip.rtx.crop.start {crop['start']} is not the spin's crop.{which} {tile} (the tile)" want = meta["spin"].get("start") if isinstance(meta.get("spin"), dict) else None if want is not None and spin.get("start") is not None and want != spin["start"]: return f"clip.rtx.meta.spin.start {want} is not the spin's start {spin['start']}" if "hd" in block and (why := rtx_clip_hd_problem(block["hd"], block)): # its HD tier: the manifest half return why if present is not None: for role, name in files.items(): data = present.get(name) if not data: return f"no {name}" if data is None else f"{name} is empty" if role == "poster": if len(data) > RTX_CLIP_POSTER_CAP: return f"{name} is {len(data)} bytes (> {RTX_CLIP_POSTER_CAP})" if webp_size(data) != tuple(size): return f"{name} is no WebP of {size[0]} x {size[1]}" else: if len(data) > RTX_CLIP_CAP: return f"{name} is {len(data)} bytes (> {RTX_CLIP_CAP})" if not mp4_ok(data): return f"{name} is no MP4" if (got := mp4_size(data)) != tuple(size): return f"{name} is {'no video track' if got is None else f'{got[0]} x {got[1]}'}, not the " \ f"full {size[0]} x {size[1]} frame" if block["v"] != (want_v := rtx_clip_v(_listed(present, files))): return f"clip.rtx.v {block['v']} is not its files' ({want_v})" if crop_moved(crop) and RTX_CLIP_CROP_FILE in present and \ (why := rtx_clip_crop_file_problem(block, present[RTX_CLIP_CROP_FILE], spin)): return why return None def rtx_clip_crop_file_problem(block, data, spin: Optional[dict] = None) -> Optional[str]: """Why ``data`` (jointsclip_rtx_crop.json's bytes) is not the squares a moved RTX clip ``block``'s crop names (v1.5): the block's manifest half (rtx_clip_problem), the file's (crop_file_frames: its ``frames_v``, {"frames": […]}), then rtx_clip_problem over the block with those squares inline (one a clip frame, squares inside the frame, frames[0] = crop.start).""" crop = block.get("crop") if isinstance(block, dict) else None if not crop_moved(crop): return "clip.rtx.crop names no frames file" if why := rtx_clip_problem(block, None, spin): return why boxes, why = crop_file_frames(data, crop["frames_v"], RTX_CLIP_CROP_FILE) return why or rtx_clip_problem({**block, "crop": crop_frames_in(crop, boxes)}, None, spin) def blender_clip(clip) -> bool: """Whether ``clip`` is a Blender moving-parts clip of its own (its ``v`` and ``files.textured``): the one a ``clip.rtx`` may sit beside (routers/library_media.py _blender_clip).""" return isinstance(clip, dict) and isinstance(clip.get("v"), str) and bool(clip["v"]) \ and isinstance(clip.get("files"), dict) and clip["files"].get("textured") == CLIP_ROLES["textured"] def rtx_clip_block(present: dict[str, bytes], info: dict, spin: Optional[dict] = None) -> dict: """The entry's ``clip.rtx`` (or ``rtxclip``: rtx_clip_entry_problem says which) from finalize's clip_rtx.json (``info``: files, size, crop, meta) and its files: ``{v, files, size, crop, meta}``, ``v`` over the files it lists. ValueError (why) when it cannot be one (rtx_clip_problem).""" files = dict(info.get("files") or {}) block = {"v": rtx_clip_v(_listed(present, files)), "files": files, "size": info.get("size"), "crop": info.get("crop"), "meta": info.get("meta")} if why := rtx_clip_problem(block, present, spin): raise ValueError(why) return block def rtx_clip_hd_v(present: dict[str, bytes]) -> str: """An RTX clip's HD ``v``: drop_v over its one file, RTX_CLIP_HD_NAME (any other name is ignored).""" return drop_v([(n, present[n]) for n in RTX_CLIP_HD_ROLES.values() if n in present]) def rtx_clip_hd_problem(hd, clip, present: Optional[dict] = None) -> Optional[str]: """Why ``hd`` cannot be the RTX clip block ``clip``'s HD tier. The manifest half (the router's listing, the Explorer's reader, publish.sh's check, rtx_clip_problem): exactly RTX_CLIP_HD_KEYS; ``v`` and ``of`` 12 hex, ``of`` the clip's own ``v`` (rendered from that clip: a re-rendered or re-cut one leaves it off); the clip's ``size`` a square [n, n] of ints and ``size`` [s, s] of ints with n < s ≤ SPIN_MAX_SIDE; ``files`` {"textured": RTX_CLIP_HD_NAME}; ``bytes`` a positive count of at most RTX_CLIP_HD_CAP. With ``present`` (finalize, build.py at splice): the file non-empty, at most RTX_CLIP_HD_CAP, an MP4 whose track is the full s x s frame, ``v`` rtx_clip_hd_v over it and ``bytes`` its length.""" if not isinstance(hd, dict): return "clip.rtx.hd is not an object" if set(hd) != set(RTX_CLIP_HD_KEYS): return f"clip.rtx.hd must hold exactly {list(RTX_CLIP_HD_KEYS)}" for key in ("v", "of"): if not (isinstance(hd[key], str) and re.fullmatch(r"[0-9a-f]{12}", hd[key])): return f"clip.rtx.hd.{key} {hd[key]!r} is not 12 hex" if not isinstance(clip, dict): return "clip.rtx.hd without its RTX clip" if hd["of"] != clip.get("v"): return f"clip.rtx.hd.of {hd['of']} is not the RTX clip's v {clip.get('v')!r}: rendered from another clip " \ "(re-rendered or re-cut since: a new HD render re-binds it)" whole = lambda x: isinstance(x, int) and not isinstance(x, bool) # noqa: E731 psize, size = clip.get("size"), hd["size"] if not (isinstance(psize, (list, tuple)) and len(psize) == 2 and all(whole(s) and s > 0 for s in psize) and psize[0] == psize[1]): return f"the RTX clip's size {psize!r} is not square: its HD is the same frame at more pixels" if not (isinstance(size, list) and len(size) == 2 and all(whole(s) for s in size) and size[0] == size[1] and psize[0] < size[0] <= SPIN_MAX_SIDE): return f"clip.rtx.hd.size {size!r} is not [s, s] with {psize[0]} < s ≤ {SPIN_MAX_SIDE}" if hd["files"] != RTX_CLIP_HD_ROLES: return f"clip.rtx.hd.files {hd['files']!r} is not {RTX_CLIP_HD_ROLES!r}" if not (whole(hd["bytes"]) and 0 < hd["bytes"] <= RTX_CLIP_HD_CAP): return f"clip.rtx.hd.bytes {hd['bytes']!r} is not a positive count of at most {RTX_CLIP_HD_CAP}" if present is None: return None data = present.get(RTX_CLIP_HD_NAME) if not data: return f"no {RTX_CLIP_HD_NAME}" if data is None else f"{RTX_CLIP_HD_NAME} is empty" if len(data) > RTX_CLIP_HD_CAP: return f"{RTX_CLIP_HD_NAME} is {len(data)} bytes (> {RTX_CLIP_HD_CAP})" if not mp4_ok(data): return f"{RTX_CLIP_HD_NAME} is no MP4" if (got := mp4_size(data)) != tuple(size): return f"{RTX_CLIP_HD_NAME} is {'no video track' if got is None else f'{got[0]} x {got[1]}'}, not the full " \ f"{size[0]} x {size[1]} frame" if hd["v"] != (want := rtx_clip_hd_v(present)): return f"clip.rtx.hd.v {hd['v']} is not its file's ({want})" if hd["bytes"] != len(data): return f"clip.rtx.hd.bytes {hd['bytes']} is not its file's {len(data)}" return None def rtx_clip_hd_block(present: dict[str, bytes], info: dict, clip: dict) -> dict: """``clip.rtx.hd`` (RTX_CLIP_HD_KEYS order) for the HD file ``present`` holds under the RTX clip block ``clip``: ``v`` rtx_clip_hd_v, ``of`` and ``size`` as ``info`` (rtxclip_hd.json) gives them, ``files`` the one role, ``bytes`` its length. ``info``'s own ``v`` / ``bytes`` / ``frames``, where given, must agree (an rtxclip_hd.json of another file: finalized over since; of another clip length). ValueError (why) when it cannot be one (rtx_clip_hd_problem with the file).""" if not isinstance(info, dict): raise ValueError(f"{RTX_CLIP_HD_META} is not an object") frames = ((clip or {}).get("meta") or {}).get("frames") if isinstance((clip or {}).get("meta"), dict) else None if "frames" in info and info["frames"] != frames: raise ValueError(f"{RTX_CLIP_HD_META} has {info['frames']!r} frames, the RTX clip {frames!r}") data = present.get(RTX_CLIP_HD_NAME) if not data: raise ValueError(f"no {RTX_CLIP_HD_NAME}" if data is None else f"{RTX_CLIP_HD_NAME} is empty") size = list(info["size"]) if isinstance(info.get("size"), (list, tuple)) else info.get("size") block = {"v": rtx_clip_hd_v(present), "of": info.get("of"), "size": size, "files": dict(RTX_CLIP_HD_ROLES), "bytes": len(data)} if why := rtx_clip_hd_problem(block, clip, present): raise ValueError(why) for key in ("v", "bytes"): if info.get(key) is not None and info[key] != block[key]: raise ValueError(f"{RTX_CLIP_HD_META} records {key} {info[key]!r}, the file is {block[key]!r} " "(finalized over since?)") return block def webp_size(data: bytes) -> Optional[tuple[int, int]]: """A WebP's (width, height) from its first chunk (VP8 lossy, VP8L lossless, VP8X extended); None when ``data`` is no WebP (or a truncated one).""" if data[:4] != b"RIFF" or data[8:12] != b"WEBP": return None chunk = data[12:16] if chunk == b"VP8 " and len(data) >= 30 and data[23:26] == b"\x9d\x01\x2a": # after the 3-byte frame tag w, h = (int.from_bytes(data[i:i + 2], "little") & 0x3FFF for i in (26, 28)) elif chunk == b"VP8L" and len(data) >= 25 and data[20] == 0x2F: bits = int.from_bytes(data[21:25], "little") w, h = (bits & 0x3FFF) + 1, ((bits >> 14) & 0x3FFF) + 1 elif chunk == b"VP8X" and len(data) >= 30: w, h = (int.from_bytes(data[i:i + 3], "little") + 1 for i in (24, 27)) else: return None return (w, h) if w and h else None def _number(x) -> bool: return isinstance(x, (int, float)) and not isinstance(x, bool) and math.isfinite(x) def _plain(x): """A number as the manifest writes it: an int when it is whole (10, not 10.0).""" return int(x) if float(x).is_integer() else float(x) def named_frames(frames: int, step_deg: float, az0_deg: float) -> dict: """{view: frame} for each named view (SPIN_NAMED_AZ) that lands on a whole frame: frame k shows camera azimuth ``az0 − k·step`` (mod 360), so the view at azimuth A is frame ``((az0 − A) / step) mod frames``.""" out = {} for view, az in SPIN_NAMED_AZ.items(): q = ((az0_deg - az) / step_deg) % frames if abs(q - round(q)) < 1e-6: out[view] = int(round(q)) % frames return out def _crop_box_problem(name: str, box) -> Optional[str]: if not (isinstance(box, list) and len(box) == 4 and all(_number(n) for n in box)): return f"crop.{name} {box!r} is not [x, y, w, h]" x, y, w, h = box if x < 0 or y < 0 or w <= 0 or h <= 0 or x + w > 1 + SPIN_CROP_SLACK or y + h > 1 + SPIN_CROP_SLACK: return f"crop.{name} {box!r} is not a box inside the frame (0–1)" return None def crop_file_bytes(frames) -> bytes: """A crop file's body (v1.5): {"frames": [[x, y, w, h], …]} as compact JSON — the bytes ``frames_v`` hashes.""" return json.dumps({"frames": frames}, separators=(",", ":")).encode() def crop_file_v(data: bytes) -> str: """A crop file's ``frames_v``: sha256 of its bytes [:12].""" return hashlib.sha256(data).hexdigest()[:12] def crop_moved(crop) -> bool: """Whether ``crop`` keeps its per-frame squares in a side file (v1.5: it names ``frames_file`` or ``frames_v``).""" return isinstance(crop, dict) and any(k in crop for k in CROP_FILE_KEYS) def crop_frames_out(crop, name: str) -> tuple: """``crop`` with its inline ``frames`` moved out (v1.5): (the crop with ``frames_file`` = ``name`` and ``frames_v`` where ``frames`` was, every other key as it was, in its order; the file's bytes, crop_file_bytes). A crop with no inline frames (none, a v1.3 one, one moved already): (it as it is, None).""" if not isinstance(crop, dict) or "frames" not in crop: return crop, None data = crop_file_bytes(crop["frames"]) out = {} for k, x in crop.items(): if k == "frames": out.update(frames_file=name, frames_v=crop_file_v(data)) else: out[k] = x return out, data def crop_frames_in(crop: dict, frames: list) -> dict: """A moved ``crop`` with ``frames`` inline again where ``frames_file`` was: what the inline rules check.""" out = {} for k, x in crop.items(): if k == "frames_file": out["frames"] = frames elif k != "frames_v": out[k] = x return out def crop_ref_problem(crop, name: str, what: str = "crop") -> Optional[str]: """Why a moved crop's reference to its side file is not one (None: a crop that names none, or a good one): ``frames_file`` exactly ``name`` (the kind's one name, CROP_FILES) and ``frames_v`` 12 hex, the two together and never beside inline ``frames``.""" if not crop_moved(crop): return None if "frames" in crop: return f"{what} has frames and {'/'.join(k for k in CROP_FILE_KEYS if k in crop)}: one or the other" if crop.get("frames_file") != name: return f"{what}.frames_file {crop.get('frames_file')!r} is not {name!r}" if not _hex12(crop.get("frames_v")): return f"{what}.frames_v {crop.get('frames_v')!r} is not 12 hex" return None def crop_file_frames(data, frames_v, name: str) -> tuple: """(the squares, None) from a crop file's bytes, else (None, why): non-empty bytes of at most CROP_FILE_CAP whose sha256[:12] is ``frames_v``, a JSON object holding exactly ``frames``, a list (the boxes are the inline rules').""" if not data: return None, f"no {name}" if data is None else f"{name} is empty" if len(data) > CROP_FILE_CAP: return None, f"{name} is {len(data)} bytes (> {CROP_FILE_CAP})" if (got := crop_file_v(bytes(data))) != frames_v: return None, f"{name} is not frames_v {frames_v} (its bytes are {got})" try: doc = json.loads(data) except ValueError: return None, f"{name} is no JSON" if not (isinstance(doc, dict) and set(doc) == {"frames"} and isinstance(doc["frames"], list)): return None, f'{name} is not {{"frames": [[x, y, w, h], …]}}' return doc["frames"], None def spin_crop_problem(crop, frames: Optional[int] = None, start: Optional[int] = None) -> Optional[str]: """Why a spin's ``crop`` (SPIN_CROP_BOXES) cannot be one: an object with ``object`` and ``square``, each [x, y, w, h] of four numbers with x, y ≥ 0, w, h > 0, x + w and y + h at most 1 (+ SPIN_CROP_SLACK); ``square`` square (w = h within SPIN_CROP_SQUARE_TOL) and holding ``object`` (within the slack). v1.4 (SPIN_CROP_V14, all three or none; every number at most 4 decimals): ``fill`` in (0.5, 1]; ``start`` a square box; ``frames`` ``frames`` square boxes (when the count is given), none smaller than ``start``, frames[``start``] (0 when not given) = ``start``. v1.5: ``frames`` may be its side file instead (crop_ref_problem: ``frames_file`` spin_crop.json and ``frames_v``); its squares are checked with the file (spin_crop_file_problem).""" keys = set(crop) if isinstance(crop, dict) else None if keys is None or not set(SPIN_CROP_BOXES) <= keys \ or keys - set(SPIN_CROP_BOXES) - set(SPIN_CROP_V14) - set(CROP_FILE_KEYS): return f"crop {crop!r} is not {{object, square}} (+ v1.4: start, frames, fill; v1.5: frames_file, frames_v)" for name in SPIN_CROP_BOXES: if why := _crop_box_problem(name, crop[name]): return why (ox, oy, ow, oh), (sx, sy, sw, sh) = crop["object"], crop["square"] if abs(sw - sh) > SPIN_CROP_SQUARE_TOL: return f"crop.square {crop['square']!r} is not square" e = SPIN_CROP_SLACK if sx > ox + e or sy > oy + e or sx + sw < ox + ow - e or sy + sh < oy + oh - e: return f"crop.square {crop['square']!r} does not hold crop.object {crop['object']!r}" if why := crop_ref_problem(crop, SPIN_CROP_FILE): return why moved = crop_moved(crop) v14 = keys & set(SPIN_CROP_V14) | ({"frames"} if moved else set()) if not v14: return None if v14 != set(SPIN_CROP_V14): return f"crop has {sorted(keys & {*SPIN_CROP_V14, *CROP_FILE_KEYS})}: v1.4's start, frames and fill go together" if not (_number(crop["fill"]) and 0.5 < crop["fill"] <= 1 and round(crop["fill"], 4) == crop["fill"]): return f"crop.fill {crop['fill']!r} is not a 4-decimal number in (0.5, 1]" if why := _crop_box_problem("start", crop["start"]): return why if any(round(x, 4) != x for x in crop["start"]): return f"crop.start {crop['start']!r} is not 4 decimals" if abs(crop["start"][2] - crop["start"][3]) > SPIN_CROP_SQUARE_TOL: return f"crop.start {crop['start']!r} is not square" if moved: return None # the squares: spin_crop_file_problem, with the file boxes = crop["frames"] if not isinstance(boxes, list) or not boxes or (frames is not None and len(boxes) != frames): return f"crop.frames is not {frames if frames is not None else 'a list of'} boxes" for k, box in enumerate(boxes): if why := _crop_box_problem(f"frames[{k}]", box): return why if any(round(x, 4) != x for x in box): return f"crop.frames[{k}] {box!r} is not 4 decimals" if abs(box[2] - box[3]) > SPIN_CROP_SQUARE_TOL or box[2] < crop["start"][2] - SPIN_CROP_SQUARE_TOL: return f"crop.frames[{k}] {box!r} is not a square at least crop.start's side" k0 = start if start is not None else 0 if not 0 <= k0 < len(boxes) or boxes[k0] != crop["start"]: return f"crop.frames[{k0}] is not crop.start {crop['start']!r}" return None def spin_crop_file_problem(crop, data, frames: Optional[int] = None, start: Optional[int] = None) -> Optional[str]: """Why ``data`` (spin_crop.json's bytes) is not the squares a moved spin ``crop`` names (v1.5): the crop's own rules (spin_crop_problem), the file's (crop_file_frames: its ``frames_v``, {"frames": […]}), then the inline rules over the crop with those squares inline (``frames`` of them, squares, none smaller than ``start``'s, frames[``start``] = ``start``).""" if not crop_moved(crop): return "the crop names no frames file" if why := spin_crop_problem(crop, frames, start): return why boxes, why = crop_file_frames(data, crop["frames_v"], SPIN_CROP_FILE) return why or spin_crop_problem(crop_frames_in(crop, boxes), frames, start) def spin_problem(present: dict[str, bytes], info) -> Optional[str]: """Why a finalized spin cannot be a manifest's ``spin``: its layout's rules (_spin_layout_problem), then — where ``info`` carries an ``hd`` (a block re-laid with its HD frames carried over; finalize's spin.json never has one) — spin_hd_problem's manifest half, its ``of`` the frames' v where ``present`` holds every frame.""" if why := _spin_layout_problem(present, info): return why if "hd" not in info: return None n = info["frames"] frames = {spin_name(k): present[spin_name(k)] for k in range(n) if spin_name(k) in present} return spin_hd_problem(info["hd"], info, parent_frames_v=spin_v(frames) if len(frames) == n else None) def _spin_layout_problem(present: Optional[dict], info) -> Optional[str]: """Why a finalized spin cannot be a manifest's ``spin``. ``info`` is spin.json (+ ``rendered_from``): ``frames`` in SPIN_FRAMES, a full turn (``frames · step_deg`` = 360), ``az0_deg`` and ``elevation_deg`` numbers, ``size`` [w, h] (each at most SPIN_MAX_SIDE), ``format`` webp when given, ``named`` (when given) the views the geometry puts on whole frames, ``start`` (when given, not null) a frame index below ``frames`` and ``start_reason`` (only beside a start) a line of at most SPIN_START_REASON_MAX characters, ``crop`` (when given, not null; v1.3) spin_crop_problem's, ``crop_source`` (when given, not null: spin.json's meta) one of SPIN_CROP_SOURCES and only beside a crop, ``files`` (when given) spin_files(start), plus — either layout — an optional hover video (``files.card``, ``card_fps``: spin_card_problem's); ``present`` every frame 0 … frames − 1 and no other (a frame past the count is a longer render's, left behind), each a non-empty WebP of ``size`` and at most SPIN_FRAME_CAP bytes; the meta (render_meta_problem) a renderer, a look, a positive ``spp`` and a ``rendered_from`` path. ``files`` listing ``sheets`` (spin_sheets.json) is the sheets layout instead (sheets_problem), which needs a ``start`` (spin_start.webp is that frame); one listing both layouts is refused. ``present`` None: the rules that need no file (spin_block_problem's).""" if not isinstance(info, dict): return f"{SPIN_META} is not an object" n, step, az0, el, size = (info.get(k) for k in ("frames", "step_deg", "az0_deg", "elevation_deg", "size")) lo, hi = SPIN_FRAMES if not (isinstance(n, int) and not isinstance(n, bool) and lo <= n <= hi): return f"frames {n!r} is not a count in {lo}–{hi}" for key, x in (("step_deg", step), ("az0_deg", az0), ("elevation_deg", el)): if not _number(x): return f"{key} {x!r} is not a number" if abs(n * step - 360) > 1e-6: return f"{n} frames of {step}° are not a full turn" if not (isinstance(size, (list, tuple)) and len(size) == 2 and all( isinstance(s, int) and not isinstance(s, bool) and 0 < s <= SPIN_MAX_SIDE for s in size)): return f"size {size!r} is not [width, height] (1–{SPIN_MAX_SIDE} each)" if info.get("format", SPIN_FORMAT) != SPIN_FORMAT: return f"format {info.get('format')!r} is not {SPIN_FORMAT}" if "named" in info and info["named"] != named_frames(n, step, az0): return f"named {info['named']!r} is not the geometry's {named_frames(n, step, az0)!r}" start, reason = info.get("start"), info.get("start_reason") if start is not None and not (isinstance(start, int) and not isinstance(start, bool) and 0 <= start < n): return f"start {start!r} is not a frame in 0–{n - 1}" if reason is not None: if start is None: return "start_reason without a start" if not (isinstance(reason, str) and reason.strip()) or len(reason) > SPIN_START_REASON_MAX: return f"start_reason is not a line of 1–{SPIN_START_REASON_MAX} characters" if info.get("crop") is not None and (why := spin_crop_problem(info["crop"], info.get("frames"), start)): return why if (source := info.get("crop_source")) is not None: if source not in SPIN_CROP_SOURCES: return f"crop_source {source!r} is not one of {list(SPIN_CROP_SOURCES)}" if info.get("crop") is None: return "crop_source without a crop" files = spin_layout_files(info.get("files")) if isinstance(files, dict) and "sheets" in files: if "pattern" in files: return "files list both layouts (pattern and sheets): a spin takes one" if start is None: return f"a sheets spin needs its start: {SPIN_START_FILE} is that frame" return sheets_problem(present, files, n, size) or spin_card_problem(present, info) or \ render_meta_problem({k: info[k] for k in STILL_META_KEYS if k in info}, "spin") if "files" in info and files != spin_files(start or 0): return f"files {info['files']!r} are not {spin_files(start or 0)!r}" if present is None: return spin_card_problem(None, info) or \ render_meta_problem({k: info[k] for k in STILL_META_KEYS if k in info}, "spin") if past := sorted(name for name in present if (k := spin_index(name)) is not None and k >= n): return f"{past[0]} is past the {n} frames (a longer render's, left behind?)" for k in range(n): name, data = spin_name(k), present.get(spin_name(k)) if not data: return f"no {name}" if data is None else f"{name} is empty" if len(data) > SPIN_FRAME_CAP: return f"{name} is {len(data)} bytes (> {SPIN_FRAME_CAP})" if (got := webp_size(data)) is None: return f"{name} is no WebP" if list(got) != list(size): return f"{name} is {got[0]}x{got[1]}, the spin {size[0]}x{size[1]}" return spin_card_problem(present, info) or \ render_meta_problem({k: info[k] for k in STILL_META_KEYS if k in info}, "spin") def sheets_dims(grid, size) -> list[tuple[str, list, int]]: """[(name, [width, height], cap)] of a sheets layout's files: each sheet cols x rows cells of ``size``, then spin_start.webp one frame.""" w, h = grid[0] * size[0], grid[1] * size[1] return [*((s, [w, h], SPIN_SHEET_CAP) for s in SPIN_SHEETS), (SPIN_START_FILE, list(size), SPIN_FRAME_CAP)] def sheets_problem(present: Optional[dict], files: dict, n: int, size) -> Optional[str]: """Why a sheets-layout spin (``files`` from spin_sheets.json, ``n`` frames of ``size``) cannot be one: ``n`` splits over the four sheets, the grid [cols, rows] holds exactly n / 4 cells and a sheet stays within SPIN_SHEET_MAX_PX and SPIN_SHEET_MAX_SIDE, ``files`` is spin_sheet_files(grid, size) (the cell the frames' size, the four fixed sheet names, ``sheet_of`` "k mod 4", the poster spin_start.webp); ``present`` each sheet a non-empty WebP of cols x rows cells of at most SPIN_SHEET_CAP bytes and spin_start.webp one frame (a WebP of ``size``, at most SPIN_FRAME_CAP). Frame files beside them (finalize writes both layouts) are ignored. ``present`` None: the grid and ``files`` alone.""" sheets = len(SPIN_SHEETS) if n % sheets: return f"{n} frames do not split over {sheets} sheets ({SPIN_SHEET_OF})" grid = files.get("grid") if not (isinstance(grid, list) and len(grid) == 2 and all(isinstance(g, int) and not isinstance(g, bool) and g > 0 for g in grid)): return f"grid {grid!r} is not [cols, rows]" cols, rows = grid if cols * rows != n // sheets: return f"a {cols}x{rows} grid is not the {n // sheets} cells a sheet holds" w, h = cols * size[0], rows * size[1] if w * h > SPIN_SHEET_MAX_PX or max(w, h) > SPIN_SHEET_MAX_SIDE: return f"a {w}x{h} sheet is over {SPIN_SHEET_MAX_PX} px or {SPIN_SHEET_MAX_SIDE} a side" if files != spin_sheet_files(grid, size): return f"files {files!r} are not {spin_sheet_files(grid, size)!r}" for name, want, cap in sheets_dims(grid, size) if present is not None else (): data = present.get(name) if not data: return f"no {name}" if data is None else f"{name} is empty" if len(data) > cap: return f"{name} is {len(data)} bytes (> {cap})" if (got := webp_size(data)) is None: return f"{name} is no WebP" if list(got) != want: return f"{name} is {got[0]}x{got[1]}, not {want[0]}x{want[1]}" return None def spin_block(present: dict[str, bytes], info: dict) -> dict: """The entry's ``spin`` (the spin contract's): ``{v, frames, step_deg, az0_deg, elevation_deg, size, format, files: {pattern, poster}, named, [start, [start_reason]], [crop, [crop_source]], meta: {renderer, look, spp, rendered_from}}`` — ``v`` spin_v over the frames, ``named`` computed from the geometry (named_frames), whole numbers as ints, the poster the frame it opens on (spin_files: ``start`` when spin.json gives one, else frame 0) and ``start`` / ``start_reason`` carried as given (none without), ``crop`` too ({object, square} and v1.4's start, frames, fill when given — v1.5: frames_file, frames_v in frames' place; whole numbers as ints; none without — it changes neither the files nor ``v``) and after it ``crop_source`` (SPIN_CROP_SOURCES: spin.json's meta.crop_source, as given; none without). No ``src``: like a still, shown whatever the hit's hash. ``info`` whose ``files`` list ``sheets`` (spin_sheets.json) makes the sheets layout's block: ``files`` spin_sheet_files(grid, size) and ``v`` spin_sheets_v (the sheets, then spin_start.webp). ``info`` whose ``files`` list ``card`` adds the hover video, either layout: ``files.card`` spin_card.mp4, ``card_fps`` (as given, else — a null too — SPIN_CARD_FPS; after ``crop``) and the card hashed into ``v`` last. ValueError (why) when it cannot be one (spin_problem).""" if why := spin_problem(present, info): raise ValueError(why) n, start = info["frames"], info.get("start") opening = {} if start is None else {"start": start, **({"start_reason": info["start_reason"]} if info.get("start_reason") is not None else {})} card = spin_card_listed(info.get("files")) if spin_layout(info.get("files")) == "sheets": v, files = spin_sheets_v(present, card), spin_sheet_files(info["files"]["grid"], info["size"]) else: frames = {spin_name(k): present[spin_name(k)] for k in range(n)} v, files = spin_v({**frames, **({SPIN_CARD: present[SPIN_CARD]} if card else {})}, card), spin_files(start or 0) crop = info.get("crop") framing = {} if crop is None else {"crop": {**{b: [_plain(x) for x in crop[b]] for b in SPIN_CROP_BOXES}, **({ "start": [_plain(x) for x in crop["start"]], **({"frames": [[_plain(x) for x in b] for b in crop["frames"]]} if "frames" in crop else # v1.5: its file {k: crop[k] for k in CROP_FILE_KEYS}), "fill": _plain(crop["fill"])} if "start" in crop else {})}, **({"crop_source": info["crop_source"]} if info.get("crop_source") is not None else {})} fps = info.get("card_fps") # a null is absent (spin_card_problem's read): the default hover = {} if not card else {"card_fps": _plain(SPIN_CARD_FPS if fps is None else fps)} return {"v": v, "frames": n, "step_deg": _plain(info["step_deg"]), "az0_deg": _plain(info["az0_deg"]), "elevation_deg": _plain(info["elevation_deg"]), "size": list(info["size"]), "format": SPIN_FORMAT, "files": {**files, "card": SPIN_CARD} if card else files, "named": named_frames(n, info["step_deg"], info["az0_deg"]), **opening, **framing, **hover, "meta": {k: info[k] for k in STILL_META_KEYS}, **({"hd": {k: info["hd"][k] for k in SPIN_HD_KEYS}} if "hd" in info else {})} SPIN_BLOCK_KEYS = ("v", "frames", "step_deg", "az0_deg", "elevation_deg", "size", "format", "files", "named") SPIN_BLOCK_OPTIONAL = ("start", "start_reason", "crop", "crop_source", "card_fps", "hd") # and ``meta``, last but # for ``hd`` def spin_block_problem(block) -> Optional[str]: """Why a manifest's ``spin``, as stored (no files at hand: the library-ingest lane's commit), is not one spin_block writes: SPIN_BLOCK_KEYS, ``meta`` (exactly STILL_META_KEYS) and nothing past SPIN_BLOCK_OPTIONAL; ``v`` 12 hex; ``format`` webp; the layout's rules that need no file (_spin_layout_problem: the geometry, ``named``, start and its reason, crop and its source, ``files``, the meta's); a listed hover video with its ``card_fps``; an ``hd`` spin_hd_problem's manifest half (no ``of`` check: a sheets block has no frames v).""" if not isinstance(block, dict): return "spin is not an object" if missing := [k for k in (*SPIN_BLOCK_KEYS, "meta") if k not in block]: return f"spin has no {missing[0]}" if extra := sorted(set(block) - set(SPIN_BLOCK_KEYS) - set(SPIN_BLOCK_OPTIONAL) - {"meta"}): return f"spin has keys spin_block never writes: {extra}" if nulls := [k for k in SPIN_BLOCK_OPTIONAL if k in block and block[k] is None]: return f"spin.{nulls[0]} is null (spin_block leaves an absent one out)" if not _hex12(block["v"]): return f"spin.v {block['v']!r} is not 12 hex" if block["format"] != SPIN_FORMAT: return f"spin.format {block['format']!r} is not {SPIN_FORMAT!r}" meta = block["meta"] if not (isinstance(meta, dict) and set(meta) == set(STILL_META_KEYS)): return f"spin.meta must hold exactly {list(STILL_META_KEYS)}" info = {**{k: x for k, x in block.items() if k not in ("v", "meta", "hd")}, **meta} if why := _spin_layout_problem(None, info): return f"spin: {why}" if spin_card_listed(block["files"]) and "card_fps" not in block: return "spin lists its hover video (files.card) without its card_fps" if "hd" in block and (why := spin_hd_problem(block["hd"], block)): return why return None def spin_hd_name(k: int) -> str: """HD frame ``k``'s file name (SPIN_HD_PATTERN).""" return SPIN_HD_PATTERN.format(k=k) def spin_hd_index(name: str) -> Optional[int]: """The HD frame index a file name carries (SPIN_HD_NAME_RE, three digits), None for any other name.""" m = SPIN_HD_NAME_RE.fullmatch(name) return int(m.group(1)) if m else None def spin_hd_v(present: dict[str, bytes]) -> str: """The HD frames' ``v``: drop_v over spin_hd_NNN.webp in frame-index order (any other name is ignored).""" frames = sorted((k, n) for n in present if (k := spin_hd_index(n)) is not None) return drop_v([(n, present[n]) for _k, n in frames]) def spin_frames_v(present: dict[str, bytes]) -> str: """A spin's FRAMES v (what ``spin.hd.of`` names): spin_v over its spin_NNN.webp alone — no card, no sheets — the pattern block's ``v`` where it lists no card, finalize's recorded ``v`` either way.""" return spin_v({n: present[n] for n in present if spin_index(n) is not None}) def _hex12(x) -> bool: return isinstance(x, str) and re.fullmatch(r"[0-9a-f]{12}", x) is not None def spin_hd_problem(hd, spin, present: Optional[dict] = None, parent_frames_v: Optional[str] = None) -> Optional[str]: """Why ``hd`` cannot be ``spin``'s HD frames (spin contract §2.3, §7). The manifest half (the router's listing, the Explorer's readSpin, publish.sh's check): exactly SPIN_HD_KEYS; ``v`` and ``of`` 12 hex; ``spin.frames`` in SPIN_FRAMES (a spin the viewer does not show has no hd); ``size`` [s, s] of ints with the parent's ``size`` square and its side < s ≤ SPIN_MAX_SIDE; ``format`` SPIN_FORMAT; ``files`` {"pattern": SPIN_HD_PATTERN}; ``bytes`` a positive count. With ``parent_frames_v`` (build.py, the parent's frames at hand): ``of`` is it. With ``present`` (finalize, build.py at splice): exactly spin_hd_000 … spin_hd_ (none past), each a non-empty WebP of ``size`` of at most SPIN_HD_FRAME_CAP bytes; ``v`` spin_hd_v over them and ``bytes`` their sum.""" if not isinstance(hd, dict): return "spin.hd is not an object" if set(hd) != set(SPIN_HD_KEYS): return f"spin.hd must hold exactly {list(SPIN_HD_KEYS)}" for key in ("v", "of"): if not _hex12(hd[key]): return f"spin.hd.{key} {hd[key]!r} is not 12 hex" if not isinstance(spin, dict): return "spin.hd without its spin" n, (lo, hi) = spin.get("frames"), SPIN_FRAMES if not (isinstance(n, int) and not isinstance(n, bool) and lo <= n <= hi): return f"the spin's frames {n!r} is not a count in {lo}–{hi}: a spin the viewer does not show has no hd" psize, size = spin.get("size"), hd["size"] whole = lambda x: isinstance(x, int) and not isinstance(x, bool) # noqa: E731 if not (isinstance(psize, (list, tuple)) and len(psize) == 2 and all(whole(s) and s > 0 for s in psize) and psize[0] == psize[1]): return f"the spin's size {psize!r} is not square: HD frames are its frames at more pixels" if not (isinstance(size, list) and len(size) == 2 and all(whole(s) for s in size) and size[0] == size[1] and psize[0] < size[0] <= SPIN_MAX_SIDE): return f"spin.hd.size {size!r} is not [s, s] with {psize[0]} < s ≤ {SPIN_MAX_SIDE}" if hd["format"] != SPIN_FORMAT: return f"spin.hd.format {hd['format']!r} is not {SPIN_FORMAT!r}" if hd["files"] != {"pattern": SPIN_HD_PATTERN}: return f"spin.hd.files {hd['files']!r} is not {{'pattern': {SPIN_HD_PATTERN!r}}}" if not (whole(hd["bytes"]) and hd["bytes"] > 0): return f"spin.hd.bytes {hd['bytes']!r} is not a positive count" if parent_frames_v is not None and hd["of"] != parent_frames_v: return f"spin.hd.of {hd['of']} is not the spin's frames v {parent_frames_v}: rendered from other frames" if present is None: return None if past := sorted(name for name in present if (k := spin_hd_index(name)) is not None and k >= n): return f"{past[0]} is past the spin's {n} frames" total = 0 for k in range(n): name, data = spin_hd_name(k), present.get(spin_hd_name(k)) if not data: return f"no {name}" if data is None else f"{name} is empty" if len(data) > SPIN_HD_FRAME_CAP: return f"{name} is {len(data)} bytes (> {SPIN_HD_FRAME_CAP})" if (got := webp_size(data)) is None: return f"{name} is no WebP" if list(got) != size: return f"{name} is {got[0]}x{got[1]}, the hd {size[0]}x{size[1]}" total += len(data) if hd["v"] != (want := spin_hd_v(present)): return f"spin.hd.v {hd['v']} is not its frames' {want}" if hd["bytes"] != total: return f"spin.hd.bytes {hd['bytes']} is not its frames' {total}" return None def spin_hd_block(present: dict[str, bytes], info: dict, spin: dict, parent_frames_v: Optional[str] = None) -> dict: """``spin.hd`` (SPIN_HD_KEYS order) for the HD frames ``present`` (spin_hd_NNN.webp) under ``spin`` (the entry's block, either layout): ``v`` spin_hd_v, ``of`` and ``size`` as ``info`` (spin_hd.json) gives them, ``bytes`` the frames' sum. ``info``'s own ``v`` / ``bytes`` / ``frames``, where given, must agree (a spin_hd.json of other frames: finalized over since). ValueError (why) when it cannot be one (spin_hd_problem, with ``parent_frames_v``: ``of`` must name it).""" if not isinstance(info, dict): raise ValueError(f"{SPIN_HD_META} is not an object") if "frames" in info and info["frames"] != (spin or {}).get("frames"): raise ValueError(f"{SPIN_HD_META} has {info['frames']!r} frames, the spin {(spin or {}).get('frames')!r}") frames = {n: present[n] for n in present if spin_hd_index(n) is not None} size = list(info["size"]) if isinstance(info.get("size"), (list, tuple)) else info.get("size") block = {"v": spin_hd_v(frames), "of": info.get("of"), "size": size, "format": SPIN_FORMAT, "files": {"pattern": SPIN_HD_PATTERN}, "bytes": sum(len(b) for b in frames.values())} if info.get("format", SPIN_FORMAT) != SPIN_FORMAT: raise ValueError(f"{SPIN_HD_META} records format {info['format']!r}, not {SPIN_FORMAT!r}") if why := spin_hd_problem(block, spin, frames, parent_frames_v): raise ValueError(why) for key in ("v", "bytes"): if info.get(key) is not None and info[key] != block[key]: raise ValueError(f"{SPIN_HD_META} records {key} {info[key]!r}, the frames are {block[key]!r} " "(finalized over since?)") return block def fake_webp(width: int, height: int, tag: bytes = b"") -> bytes: """A minimal extended WebP webp_size reads (RIFF, VP8X with the canvas size, an unknown chunk carrying ``tag``): the self-tests' spin frames.""" vp8x = b"VP8X" + (10).to_bytes(4, "little") + bytes(4) + (width - 1).to_bytes(3, "little") + \ (height - 1).to_bytes(3, "little") pad = tag + bytes(len(tag) % 2) body = b"WEBP" + vp8x + b"TAG " + len(tag).to_bytes(4, "little") + pad return b"RIFF" + len(body).to_bytes(4, "little") + body