Ctrl K
ComunidadGuías…

cookbook/01_adapter_facade.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Copyright (c) 2026 OpenNVR
# SPDX-License-Identifier: Apache-2.0
"""`Adapter` — the facade, and the only file most adapters need.

Demonstrates: `Adapter`, `@Adapter.load`, `@Adapter.on_image`,
`@Adapter.check_hardware`, `@Adapter.on_shutdown`, `InferCall`,
`InferCall.detection`, `Overloaded`, `Adapter.app`.

An adapter wraps a model and answers the AI Adapter Contract. The
facade derives everything the contract needs except the model itself:
the fingerprint, health, the hardware verdict, the modalities, the body
shape and the error taxonomy.

Run it:

    opennvr-adapter dev            # drive it in-process
    uvicorn 01_adapter_facade:app --port 9000
"""
from opennvr_adapter_sdk import Adapter, Overloaded

adapter = Adapter(
    "fall-detection",
    version="1.0.0",
    vendor="ACME Vision",
    license="Apache-2.0",
    # An app asks for a TASK, never for your adapter by name — matching
    # an existing §5.x convention is what makes this model a drop-in
    # replacement for somebody else's.
    tasks=["object_detection"],
    framework="onnxruntime",
    # Hashed for the fingerprint KAI-C uses to notice a model changing
    # underneath a deployment, and reported as size_mb in /capabilities.
    weights="models/fall.onnx",
    gpu=False,
    max_inflight=1,
)


@adapter.load()
def load():
    """Load once, at startup. Whatever this returns is ``call.model``.

    Import heavy ML libraries HERE rather than at module top: a missing
    dependency then shows up as a red /health carrying the real error,
    instead of a container that will not import at all."""
    import onnxruntime as ort

    return ort.InferenceSession(adapter.weights,
                                providers=["CPUExecutionProvider"])


@adapter.on_image()
def detect(call):
    """THE MODEL. Called once per frame.

    Returning a list is the §5.1 detection convention; returning a dict
    is a result shape of your own."""
    if len(call.image) < 4:
        # A ValueError becomes a 400, which tells KAI-C NOT to retry.
        # Misclassifying a bad frame as a 500 produces a retry storm.
        raise ValueError("frame is empty")

    threshold = float(call.param("confidence_threshold", 0.5))
    outputs = call.model.run(None, {"images": _preprocess(call.image)})

    return [
        # Coordinates are NORMALIZED (0–1 of the frame). Divide pixel
        # values by the frame size, or every box lands in the wrong
        # place on the operator's screen.
        call.detection("fallen", score, x, y, w, h)
        for score, (x, y, w, h) in _decode(outputs)
        if score >= threshold
    ]


@adapter.check_hardware()
def check(model):
    """Optional: the real hardware requirement, tested.

    Return an `HardwareEvaluationResponse`, a `HardwareVerdict`, a bool,
    or a ``(verdict, reasoning)`` pair. Without this the facade derives
    a verdict from whether the model loaded."""
    import onnxruntime as ort

    providers = ort.get_available_providers()
    if "CUDAExecutionProvider" in providers:
        return "ok", "CUDA is available; expect ~120 fps at 640x640."
    return "warn", ("CPU only — expect ~8 fps at 640x640. Fine for one or "
                    "two cameras, not for a site.")


@adapter.on_shutdown()
def release(model):
    """Runs on the way out, with the loaded model."""
    del model


def busy_example(call):
    """Backpressure, when it is honest to shed load. A 503 with a retry
    hint is very different from a model failure: KAI-C backs off and
    comes back rather than counting an error."""
    raise Overloaded("inference queue is full", retry_after_ms=250)


def _preprocess(jpeg: bytes):
    raise NotImplementedError("your preprocessing")


def _decode(outputs):
    return []


# The ASGI application. `uvicorn 01_adapter_facade:app`
app = adapter.app