"""Bounded GLB metadata preflight; does not load textures or allocate mesh arrays.""" import json import struct from pathlib import Path MAX_FACES = 500_000 MAX_VERTICES = 500_000 MAX_JSON = 16 * 1024 * 1024 def inspect_glb(path): path = Path(path) size = path.stat().st_size with path.open("rb") as stream: header = stream.read(20) if len(header) != 20: raise ValueError("Truncated GLB header") magic, version, length, json_length, chunk = struct.unpack("<4sIIII", header) if magic != b"glTF" or version != 2 or length != size or chunk != 0x4E4F534A: raise ValueError("Invalid GLB 2.0 header or file length") if json_length > MAX_JSON or json_length > size - 20: raise ValueError("GLB metadata is too large or truncated") doc = json.loads(stream.read(json_length)) meshes = doc.get("meshes", []) accessors = doc.get("accessors", []) vertices = faces = 0 unsupported = False for mesh in meshes: for primitive in mesh.get("primitives", []): if primitive.get("mode", 4) != 4: unsupported = True continue position = primitive.get("attributes", {}).get("POSITION") if position is None: raise ValueError("Mesh primitive has no POSITION accessor") count = int(accessors[position]["count"]) index = primitive.get("indices") indices = int(accessors[index]["count"]) if index is not None else count if count < 0 or indices < 0 or indices % 3: raise ValueError("Invalid vertex or triangle count") vertices += count faces += indices // 3 # Count node instances because upstream merges transformed scene geometry. nodes = doc.get("nodes", []) instances = {} for node in nodes: if "mesh" in node: instances[node["mesh"]] = instances.get(node["mesh"], 0) + 1 instantiated_faces = 0 instantiated_vertices = 0 for i, mesh in enumerate(meshes): multiplicity = max(1, instances.get(i, 0)) for primitive in mesh.get("primitives", []): pos = primitive.get("attributes", {}).get("POSITION") if pos is None: continue count = int(accessors[pos]["count"]) index = primitive.get("indices") n = int(accessors[index]["count"]) if index is not None else count instantiated_vertices += count * multiplicity if primitive.get("mode", 4) == 4: instantiated_faces += n // 3 * multiplicity required_extensions = doc.get("extensionsRequired", []) unsupported = unsupported or any(x in required_extensions for x in ("KHR_draco_mesh_compression", "EXT_meshopt_compression")) dense = instantiated_faces > MAX_FACES or instantiated_vertices > MAX_VERTICES or size > 512 * 1024**2 warning = ("Preprocessing/decimation required: this input exceeds the conservative " "500,000-face or 500,000-vertex limit. The original is preserved; " "inference has not started.") if dense else "" if unsupported: warning = "Preprocessing required: export uncompressed triangle geometry before inference." return dict(file_size_bytes=size, mesh_count=len(meshes), vertices=vertices, triangles=faces, instantiated_vertices=instantiated_vertices, instantiated_triangles=instantiated_faces, materials=len(doc.get("materials", [])), textures_present=bool(doc.get("textures") or doc.get("images")), density_risk="high" if dense else "within conservative limit", inference_allowed=bool(meshes) and faces > 0 and not dense and not unsupported, warning=warning)