Spaces:
Running on RTX PRO 6000
Running on RTX PRO 6000
Batch 10-16 15:0xZ: combined loader head 69e9ac6a (#89 HD split + GLB skip, W6 joints recount, #97 replace rule) + UI dist slim-ui-d1ae0ba9 (KTX2 pool 2, joint counts); index run pinned via RESTORE_RUN_ID
3da556c verified Download pipeline/libmedia_rules.py from nvidia/simready-central: direct link, hf CLI and curl.
- Browser
- Download file 72.9 kB
-
https://huggingface.co/spaces/nvidia/simready-central/resolve/main/pipeline/libmedia_rules.py
- Command line
-
hf download hf://spaces/nvidia/simready-central/pipeline/libmedia_rules.py
-
curl -L -o libmedia_rules.py https://huggingface.co/spaces/nvidia/simready-central/resolve/main/pipeline/libmedia_rules.py
72.9 kB
| # 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/<id>/, an RTX clip's under clips/<id>/) and served at | |
| # /library_media/<index>/<id>/<name>?v=<frames_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/<index>/spinhd/<id>/. 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/<index>/rtxcliphd/<id>/. 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_<frames − 1> (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 | |