[project] name = "opennvr-ai-adapter" # The reference adapter server. Versioned with the OpenNVR release train # (v0.1.0 = the first public cut). Not published to PyPI — it ships as the # per-adapter GHCR images built on each `v*` tag. The standalone SDK under # opennvr_adapter_sdk/ versions independently (1.x = AI Adapter Contract v1) # and IS published to PyPI, off `sdk-v*` tags. version = "0.1.10" description = "Clean Architecture AI Adapter for OpenNVR" requires-python = ">=3.11, <3.13" # ── Core dependencies ────────────────────────────────────────────────────────── # These are installed in EVERY deployment regardless of which adapters are used. # Keep this list lean: only what the FastAPI server, routing, and base adapter # infrastructure needs. dependencies = [ "fastapi==0.127.0", "uvicorn==0.40.0", "pydantic==2.12.5", "PyYAML==6.0.3", "httpx==0.28.1", "requests==2.32.5", "pillow==12.0.0", "numpy==1.26.4", "opencv-python==4.10.0.84", # Required by FastAPI for any multipart/form-data route. Every # adapter that accepts image/audio body via the SDK's body parser # (insightface /faces/register + every /infer with multipart) and # every conformance test that POSTs multipart hits the SDK's # ``await request.form()`` call which assert-raises without this # package. Not a transitive dep of fastapi as of 0.127, even # though many older installs happened to drag it in via Starlette. # Pin per FastAPI's own minimum: # https://fastapi.tiangolo.com/tutorial/request-forms-and-files/ "python-multipart>=0.0.7", ] # ── Optional adapter dependency groups ──────────────────────────────────────── # Install only what your deployment actually uses: # # uv sync --extra yolo → YOLOv8 person detection (ONNX, CPU-friendly) # uv sync --extra yolo11 → YOLOv11 person counting with ByteTrack # uv sync --extra pose → YOLO11n-pose human keypoints (ONNX, CPU-first) # uv sync --extra package → package-detection parcels on a doorstep (ONNX, CPU, on demand) # uv sync --extra clip --extra cpu → CLIP embeddings for semantic search # uv sync --extra face → InsightFace recognition (ArcFace, Buffalo-L) # uv sync --extra blip → BLIP scene captioning (local, needs torch) # uv sync --extra huggingface → HuggingFace cloud inference proxy # uv sync --extra stt → Whisper speech-to-text (faster-whisper, CPU or GPU) # uv sync --extra tts → Piper text-to-speech (ONNX, CPU-friendly) # uv sync --extra lpr → fast-plate-ocr license-plate recognition (ONNX, CPU-only) # uv sync --extra cpu → PyTorch CPU-only build (needed by yolo11+blip) # uv sync --extra gpu → PyTorch CUDA build (needed by yolo11+blip on GPU) # uv sync --extra all → Everything (backward-compatible default) # # Note: the Ollama LLM adapter (ollama_adapter) needs NO extras — it only speaks # HTTP to a local Ollama daemon (https://ollama.ai) via httpx, already in core. # # Multi-profile example: # uv sync --extra yolo --extra face → detection + recognition, no torch # uv sync --extra yolo11 --extra blip --extra cpu → counting + captions on CPU [project.optional-dependencies] # YOLOv8 ONNX adapter (yolov8_adapter) — lightweight, no torch needed. # What it needs to SERVE, and nothing else: adapters/yolov8/ imports # onnxruntime, cv2 and numpy, and adapters/yolov8/Dockerfile installs # onnxruntime alone. onnx and onnxslim used to sit here as well; they are # the ultralytics EXPORT step (download_models.py), which also needs # ultralytics — never listed here either, because it drags torch in — so # the two of them were dead weight when serving and still not enough to # export with. The export recipe names all three where it lives. yolo = [ "onnxruntime==1.23.2", ] # YOLOv11 PyTorch adapter with ByteTrack (yolov11_adapter) # Requires torch — add --extra cpu or --extra gpu as well yolo11 = [ "ultralytics==8.3.240", "matplotlib==3.10.8", "networkx==3.6.1", "protobuf>=6.33.2", ] # YOLO-pose ONNX adapter (adapters/yolo_pose) — human keypoints, CPU-first. # What the adapter actually needs to SERVE, and nothing else (CONTRIBUTING, # Path A): onnxruntime to run the graph, plus the OpenCV/numpy already in # core. adapters/yolo_pose/ imports exactly onnxruntime, cv2 and numpy — # and the image (adapters/yolo_pose/Dockerfile) installs no onnx, no # onnxslim, no ultralytics, and passes conformance. # # The .pt -> .onnx export is an authoring task on a development machine, # not a runtime dep, so its three packages (ultralytics, onnx, onnxslim) # live with the recipe in adapters/yolo_pose/README.md. Listing two of # the three here would be the worst of both: dependencies that are dead # weight in the serving profile and still not enough to export with. pose = [ "onnxruntime==1.23.2", # Serving dep, not a model dep: bare uvicorn has no WebSocket # implementation, so /infer/stream answers the upgrade with a 404 # unless one is installed. Streaming is this adapter's primary # path, so it is not optional here. "websockets>=12,<18", ] # Package-detection ONNX adapter (adapters/package_detection) — parcels. # Same serving stack as yolo/pose: onnxruntime plus the OpenCV/numpy in # core, nothing else. No websockets — it serves HTTP /infer only. The # training/export recipe (ultralytics, onnx, onnxslim, roboflow) is an # authoring task in adapters/package_detection/train_package_model.py. package = [ "onnxruntime==1.23.2", ] # CLIP embedding adapter (adapters/clip) — the `embed` task: one vector # per frame, and one per QUERY IN WORDS, in the same space. # Requires torch — add --extra cpu or --extra gpu as well, exactly like # yolo11, blip and vlm. torch is NOT listed here: it is pinned once, in # the cpu/gpu extras, so the operator picks the wheel their hardware # wants instead of every extra fighting over it. # # pillow is not listed either — it is a core dependency at an exact pin # (see [project.dependencies]). Restating it here as a range is what # broke the lock: `pillow>=10,<12` excludes the pinned 12.0.0, and uv # resolves EVERY extra even for `uv sync --extra dev`, so a conflict in # an unused extra fails the whole install. clip = [ "open-clip-torch>=2.24,<4", ] # InsightFace face recognition adapter (insightface_adapter). # Unlike yolo/pose, the `onnx` here is NOT an export leftover: insightface # itself requires onnx>=1.13, so this line is a deliberate version pin on # a dependency the adapter really does load. Leave it. face = [ "insightface>=0.7.3", "onnx==1.20.0", "onnxruntime==1.23.2", "scipy==1.16.3", "polars==1.36.1", ] # BLIP image captioning adapter (blip_adapter) # Requires torch — add --extra cpu or --extra gpu as well blip = [ "transformers==5.0.0", ] # VLM open-vocabulary detection adapter (adapters/vlm) — OWL-ViT v2. # Requires torch — add --extra cpu or --extra gpu as well. Pillow for # image decode. GPU strongly recommended (CPU inference is slow). vlm = [ "transformers==5.0.0", "pillow>=10.0.0", ] # HuggingFace cloud inference proxy (huggingface_adapter) huggingface = [ "huggingface_hub==1.3.4", ] # Whisper speech-to-text adapter (whisper_adapter) # faster-whisper ships its own CTranslate2 runtime — no torch required on CPU. # For CUDA inference, also install --extra gpu so torch.cuda.is_available() gates # the "auto" device picker. av is pulled transitively by faster-whisper for audio # decoding (WAV/MP3/FLAC/OGG/M4A). stt = [ "faster-whisper>=1.1.0", ] # Piper text-to-speech adapter (piper_adapter) # piper-tts reuses the onnxruntime we already depend on transitively via yolo, # so adding TTS costs only the Python wrapper + the voice file (~25 MB/voice). tts = [ "piper-tts>=1.2.0", ] # fast-plate-ocr license-plate recognition adapter (fast_plate_ocr_adapter). # Brings its own onnxruntime; weights (~3 MB per bundle) are downloaded on # first load() and cached locally. CPU-only — no torch needed. # # Python-version marker: fast-plate-ocr 2.x publishes wheels for Python # 3.11 across all supported platforms (Linux x86_64 + aarch64, macOS), # but as of 2026-05 has NOT shipped any Python 3.12 wheels. Empirically # verified by the universal-resolve failure we saw on the CI matrix: # every Python-3.12 platform variant uv probed (darwin, linux x86_64, # linux aarch64) reported "only fast-plate-ocr<=1.1.0 is available". # # The marker below makes the universal-lockfile resolution skip fast- # plate-ocr on Python 3.12 entirely so the rest of the install can # succeed across the deployment matrix. Operators who need LPR have # two options: # 1. Run the adapter on Python 3.11 (pyproject's lower bound) until # upstream publishes 3.12 wheels — straightforward, the Docker # image can pin to 3.11 in its base image. # 2. Cross-build the wheel locally and pin via `tool.uv.sources`. # # Empirically verified against PyPI: the LATEST published version of # fast-plate-ocr is 1.1.0 — there is no 2.x release. The previous # `>=2.0` pin was speculative against an unreleased 2.x API and made # the universal lockfile resolution unsatisfiable on every platform # (uv kept correctly reporting "only fast-plate-ocr<=1.1.0 is # available"). Verified via the PyPI JSON API: # https://pypi.org/pypi/fast-plate-ocr/json # Use `>=1.1.0` so 1.1.0 satisfies today and any future 2.x ships # naturally if/when upstream releases it. # # KNOWN ADAPTER GAP — TODO before LPR ships end-to-end: # adapters/fast_plate_ocr/service.py:252 calls # ``self._recognizer.run(image_bytes, return_confidence=True)`` with # raw bytes. fast-plate-ocr 1.1.0's ``run()`` accepts # ``str | list[str] | numpy.ndarray | list[numpy.ndarray]`` only, # NOT raw bytes. The adapter needs ``cv2.imdecode`` (or PIL.Image → # numpy array) before passing to ``run()`` — see how the InsightFace # adapter handles the same body shape. Tracked separately; the CI # fix below only addresses the universal-resolve break. # # CI is unaffected by the adapter gap above: CI runs # ``uv sync --extra dev`` (no [lpr]), and the fast-plate-ocr adapter # tests stub the recognizer entirely via # tests/_fast_plate_ocr_service_fixtures.py so they exercise the # wrapper logic without touching real fast-plate-ocr code. lpr = [ # The [onnx] extra is REQUIRED: since 1.x, onnxruntime is optional # upstream ("pip install 'fast-plate-ocr[onnx]'" per its own error # message) — without it LicensePlateRecognizer raises # ModuleNotFoundError at load() and every inference 503s. Found in # the field: the published adapter image could never read a plate. "fast-plate-ocr[onnx]>=1.1.0", # Plate LOCALIZATION (same author, ONNX/CPU): the platform sends # whole-vehicle crops, and OCR without a detection stage # hallucinates on them — see adapters/fast_plate_ocr/service.py. "open-image-models[onnx]>=0.6.0", ] # PyTorch CPU-only — required by yolo11 and blip on CPU-only machines cpu = [ "torch==2.9.1+cpu", "torchvision==0.24.1+cpu", ] # PyTorch CUDA — required by yolo11 and blip on GPU machines gpu = [ "torch==2.9.1", "torchvision==0.24.1", ] # Development tools dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.23.5", "pytest-cov>=4.1.0", "httpx>=0.28.1", ] # Backward-compatible "install everything" profile # Equivalent to the old plain `uv sync` that had all deps in [project.dependencies] all = [ "opennvr-ai-adapter[yolo,yolo11,pose,package,face,blip,vlm,clip,huggingface,stt,tts,lpr]", ] # ── PyTorch index source ─────────────────────────────────────────────────────── [tool.uv.sources] torch = { index = "pytorch-cpu" } torchvision = { index = "pytorch-cpu" } [[tool.uv.index]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true # ── Pytest config ────────────────────────────────────────────────────────────── # The project ships several top-level packages (``app``, ``adapters``, # ``conformance``, ``opennvr_adapter_sdk``) that live at the repo root. # Without a ``[build-system]`` + ``[tool.hatch.build.targets.wheel]`` # packages declaration, ``uv sync`` installs the declared dependencies # but does NOT install the project's own code as an importable # package — so ``uv run pytest`` can't find ``app.utils.loader`` from # tests/conftest.py. Adding the repo root to pythonpath here is the # minimum fix; a proper hatch/setuptools packaging declaration is a # separate (larger) cleanup. [tool.pytest.ini_options] pythonpath = ["."] # Two adapters keep their tests beside their code; without this line # the ci.yml ``pytest tests/`` never collected them. testpaths = ["tests", "adapters/ollamavlm/tests", "adapters/vlm/tests"] asyncio_mode = "auto"