WIP checkpoint: US-1: Asset Import Pipeline \u2014 glTF upload endpoint with metadata extraction

This commit is contained in:
2026-08-19 13:03:06 +00:00
parent aa2f79b125
commit 79cdd4a132
+77
View File
@@ -37,6 +37,7 @@ class AssetResponse(BaseModel):
file_path: str file_path: str
file_size: Optional[int] file_size: Optional[int]
mime_type: Optional[str] mime_type: Optional[str]
metadata_json: Optional[dict]
world_id: Optional[int] world_id: Optional[int]
owner_id: int owner_id: int
is_public: bool is_public: bool
@@ -64,6 +65,75 @@ def _sanitize_filename(filename: str) -> str:
return safe or "file" return safe or "file"
def _extract_gltf_metadata(content: bytes) -> Optional[dict]:
"""Extract metadata from a glTF/GLB file.
Returns a dict with mesh_count, vertex_count, has_animation, texture_refs
if the file is a valid glTF/GLB, or None otherwise.
"""
try:
import gltflib
except ImportError:
return None
try:
model = gltflib.GLFModel.from_json(content)
except Exception:
return None
# Count meshes
meshes = model.meshes or []
mesh_count = len(meshes)
# Count total vertices across all primitives
vertex_count = 0
for mesh in meshes:
primitives = mesh.primitives or []
for primitive in primitives:
indices = primitive.indices
if indices is not None:
# Use index count
accessor = model.accessors[indices] if indices < len(model.accessors or []) else None
if accessor and accessor.count is not None:
vertex_count += accessor.count
else:
vertex_count += 1 # fallback
else:
# No indices — count from POSITION attribute
if primitive.attributes and primitive.attributes.POSITION is not None:
accessor = model.accessors[primitive.attributes.POSITION] if primitive.attributes.POSITION < len(model.accessors or []) else None
if accessor and accessor.count is not None:
vertex_count += accessor.count
else:
vertex_count += 1 # fallback
# Check for animations
animations = model.animations or []
has_animation = len(animations) > 0
# Collect texture references
texture_refs = []
textures = model.textures or []
for texture in textures:
if texture.source is not None and texture.source < len(model.images or []):
image = model.images[texture.source]
ref = {}
if image.name:
ref["name"] = image.name
if image.uri:
ref["uri"] = image.uri
if image.mimeType:
ref["mime_type"] = image.mimeType
texture_refs.append(ref)
return {
"mesh_count": mesh_count,
"vertex_count": vertex_count,
"has_animation": has_animation,
"texture_refs": texture_refs,
}
# --- Endpoints --- # --- Endpoints ---
@router.post("/", response_model=AssetResponse, status_code=201) @router.post("/", response_model=AssetResponse, status_code=201)
@@ -204,6 +274,13 @@ async def upload_asset_file(
asset.mime_type = file.content_type asset.mime_type = file.content_type
asset.file_size = file_size asset.file_size = file_size
# Extract glTF metadata for glTF/GLB files.
filename_lower = (file.filename or "").lower()
if filename_lower.endswith(".glb") or filename_lower.endswith(".gltf"):
metadata = _extract_gltf_metadata(content)
if metadata is not None:
asset.metadata_json = str(metadata)
await db.commit() await db.commit()
await db.refresh(asset) await db.refresh(asset)
return asset return asset