fix: model weights setup — export scripts, ORT compat, bootstrap

- Add export_models.py for OpenCLIP ViT-B/32 and YOLOv8n ONNX export
- Fix ArgMax(13) ORT ARM64 incompatibility by passing eot_indices as a
  separate ONNX input (computed outside the graph in embed.py)
- Use legacy TorchScript exporter (dynamo=False) for IR version 9 compat
- Upgrade onnxruntime to 1.18.1
- Rewrite bootstrap_models.py with clear separation of auto-downloadable
  models (YuNet, SFace) vs manually-exported ones (OpenCLIP, YOLOv8n)
- Wire bootstrap into worker CMD (runs before Celery)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 09:41:25 +02:00
parent 29177f0c1a
commit 2a6661f779
5 changed files with 233 additions and 38 deletions

View File

@@ -4,7 +4,11 @@ Download vision model weights on first worker boot.
Run as: python -m app.services.vision.bootstrap_models
Or called from the vision worker entrypoint before Celery starts.
Downloads are idempotent — existing files with matching sizes are skipped.
Downloads are idempotent — existing files are skipped.
For models that require export (OpenCLIP, YOLOv8n), see export_models.py.
Those must be exported once on any machine with pip, then placed in
the models volume before the worker starts.
"""
import logging
import os
@@ -15,35 +19,30 @@ from app.config import settings
logger = logging.getLogger(__name__)
# (relative_path, url, expected_size_bytes_approx)
# Sizes are approximate — used only for skip-if-exists checks, not integrity.
MODELS = [
# OpenCLIP ViT-B/32 — visual and textual encoders (ONNX)
# These must be exported manually via export_openclip.py (see below).
# Placeholder entries — bootstrap will warn if missing.
("embed/visual.onnx", None, None),
("embed/textual.onnx", None, None),
# YOLOv8n — object detection
# Export: `yolo export model=yolov8n.pt format=onnx imgsz=640`
# Placeholder — must be exported from ultralytics offline.
("detect/yolov8n.onnx", None, None),
# YuNet — face detection (Apache 2.0, opencv_zoo)
# (relative_path, url, description)
# Models with url=None must be pre-exported via export_models.py.
DOWNLOADS = [
# YuNet — face detection (Apache 2.0, opencv_zoo, ~233 KB)
(
"face/yunet.onnx",
"https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx",
233_000,
"YuNet face detector",
),
# SFace — face recognition (Apache 2.0, opencv_zoo)
# SFace — face recognition (Apache 2.0, opencv_zoo, ~37 MB)
(
"face/sface.onnx",
"https://github.com/opencv/opencv_zoo/raw/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx",
37_000_000,
"SFace face recognizer",
),
]
# Models that need manual export via export_models.py
EXPORTS = [
("embed/visual.onnx", "OpenCLIP ViT-B/32 visual encoder"),
("embed/textual.onnx", "OpenCLIP ViT-B/32 textual encoder"),
("detect/yolov8n.onnx", "YOLOv8n object detector"),
]
def bootstrap(models_dir: str | None = None):
"""Ensure all model files are present. Download what we can, warn about
@@ -51,32 +50,46 @@ def bootstrap(models_dir: str | None = None):
base = Path(models_dir or settings.vision.models_dir)
base.mkdir(parents=True, exist_ok=True)
for rel_path, url, expected_size in MODELS:
# Download auto-downloadable models
for rel_path, url, desc in DOWNLOADS:
dest = base / rel_path
dest.parent.mkdir(parents=True, exist_ok=True)
if dest.exists():
logger.debug("Model already exists: %s", dest)
logger.debug("Already exists: %s (%s)", dest, desc)
continue
if url is None:
logger.warning(
"Model file %s not found and has no auto-download URL. "
"See bootstrap_models.py for export instructions.",
dest,
)
continue
logger.info("Downloading %s%s", url, dest)
logger.info("Downloading %s%s", desc, dest)
try:
urlretrieve(url, str(dest))
actual = dest.stat().st_size
logger.info("Downloaded %s (%d bytes)", rel_path, actual)
size_kb = dest.stat().st_size / 1024
logger.info("Downloaded %s (%.0f KB)", desc, size_kb)
except Exception as e:
logger.error("Failed to download %s: %s", rel_path, e)
logger.error("Failed to download %s: %s", desc, e)
if dest.exists():
dest.unlink()
# Check for manually-exported models
missing = []
for rel_path, desc in EXPORTS:
dest = base / rel_path
if not dest.exists():
missing.append((rel_path, desc))
if missing:
logger.warning(
"Missing %d model(s) that require manual export via export_models.py:",
len(missing),
)
for rel_path, desc in missing:
logger.warning(" %s%s", base / rel_path, desc)
logger.warning(
"Run: python -m app.services.vision.export_models --models-dir %s",
base,
)
else:
logger.info("All model files present in %s", base)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)