Files
HartOMat/backend/app/domains/materials/tasks.py
T
Hartmut a18d4c23ec feat(K): Blender Asset Library + production exports (GLB + .blend)
- feat(migration): 045_asset_libraries — new asset_libraries table (blend_file_path, catalog JSONB)
- feat(model): AssetLibrary SQLAlchemy model in domains/materials/models.py
- feat(api): POST/GET/PATCH/DELETE /api/asset-libraries + /upload-blend + /refresh-catalog endpoints
- feat(celery): refresh_asset_library_catalog task on thumbnail_rendering queue — runs Blender headless
- feat(blender): catalog_assets.py — extracts asset-marked materials + node_groups from .blend
- feat(blender): asset_library.py — apply_asset_library_materials + apply_asset_library_node_groups helpers
- feat(blender): export_gltf.py — STEP→STL→GLB production export with optional asset library
- feat(blender): export_blend.py — STEP→STL→.blend production export with pack_all()
- feat(frontend): api/assetLibraries.ts — full CRUD API client
- feat(frontend): AssetLibraryPanel in Admin.tsx — upload, refresh, expand catalog view
- docs: Blender asset_data marking requirement learning in LEARNINGS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:56:26 +01:00

106 lines
3.4 KiB
Python

"""Celery tasks for asset library management."""
from __future__ import annotations
import json
import logging
import subprocess
import uuid
from pathlib import Path
from celery import shared_task
logger = logging.getLogger(__name__)
CATALOG_SCRIPT = Path(__file__).parent.parent.parent.parent.parent / "render-worker" / "scripts" / "catalog_assets.py"
@shared_task(
name="app.domains.materials.tasks.refresh_asset_library_catalog",
queue="thumbnail_rendering",
bind=True,
max_retries=2,
default_retry_delay=30,
)
def refresh_asset_library_catalog(self, asset_library_id: str) -> None:
"""Run Blender headless to extract catalog from a .blend asset library.
Updates the `catalog` JSONB column of the AssetLibrary record.
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from app.domains.materials.models import AssetLibrary
from app.config import settings
sync_url = settings.database_url.replace("postgresql+asyncpg://", "postgresql://")
try:
engine = create_engine(sync_url)
with Session(engine) as db:
lib = db.get(AssetLibrary, uuid.UUID(asset_library_id))
if not lib:
logger.warning("AssetLibrary %s not found", asset_library_id)
return
blend_path = lib.blend_file_path
engine.dispose()
if not blend_path or not Path(blend_path).exists():
logger.warning("AssetLibrary %s: blend file not found at %s", asset_library_id, blend_path)
return
# Determine Blender binary
import os
blender_bin = os.environ.get("BLENDER_BIN", "blender")
result = subprocess.run(
[
blender_bin,
"--background",
"--python", str(CATALOG_SCRIPT),
"--", blend_path,
],
capture_output=True,
text=True,
timeout=120,
)
if result.returncode != 0:
logger.error("catalog_assets.py failed (exit %d):\n%s", result.returncode, result.stderr)
return
# Parse catalog JSON from stdout (last line that starts with '{')
catalog = None
for line in reversed(result.stdout.splitlines()):
line = line.strip()
if line.startswith("{"):
try:
catalog = json.loads(line)
break
except json.JSONDecodeError:
continue
if catalog is None:
logger.error("catalog_assets.py: no JSON found in output:\n%s", result.stdout)
return
# Persist catalog
engine2 = create_engine(sync_url)
with Session(engine2) as db:
lib = db.get(AssetLibrary, uuid.UUID(asset_library_id))
if lib:
lib.catalog = catalog
db.commit()
logger.info(
"AssetLibrary %s catalog updated: %d materials, %d node_groups",
asset_library_id,
len(catalog.get("materials", [])),
len(catalog.get("node_groups", [])),
)
engine2.dispose()
except subprocess.TimeoutExpired:
logger.error("catalog_assets.py timed out for library %s", asset_library_id)
raise self.retry(countdown=60)
except Exception as exc:
logger.exception("refresh_asset_library_catalog failed: %s", exc)
raise self.retry(exc=exc)