@trillboards/edge-sdk 0.2.81 → 0.2.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/detector/README.md +108 -21
- package/deploy/detector/bench/verify_against_reference.py +6 -1
- package/deploy/detector/pyproject.toml +1 -1
- package/deploy/detector/tests/test_loader.py +188 -0
- package/deploy/detector/tests/test_modelpath.py +48 -3
- package/deploy/detector/tests/test_registry.py +880 -0
- package/deploy/detector/tests/test_server.py +163 -7
- package/deploy/detector/tests/test_session.py +29 -10
- package/deploy/detector/trillboards_detector/__init__.py +1 -1
- package/deploy/detector/trillboards_detector/cli.py +49 -2
- package/deploy/detector/trillboards_detector/detections.py +98 -0
- package/deploy/detector/trillboards_detector/errors.py +38 -0
- package/deploy/detector/trillboards_detector/labels.py +239 -24
- package/deploy/detector/trillboards_detector/loader.py +186 -148
- package/deploy/detector/trillboards_detector/modelpath.py +32 -0
- package/deploy/detector/trillboards_detector/registry.py +836 -0
- package/deploy/detector/trillboards_detector/server.py +59 -40
- package/deploy/detector/trillboards_detector/session.py +84 -55
- package/dist/EdgeAgent.js +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/package.json +1 -1
|
@@ -32,8 +32,7 @@ import sys as _sys
|
|
|
32
32
|
import threading
|
|
33
33
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
34
34
|
|
|
35
|
-
from .errors import DetectorNotReady
|
|
36
|
-
from .labels import KNOWN_CLASS_NAMES, resolve_requested_classes
|
|
35
|
+
from .errors import DetectorNotReady, UnknownClasses
|
|
37
36
|
|
|
38
37
|
#: Loopback only. litert-lm owns 9379; this is a SECOND service, not a route on
|
|
39
38
|
#: that one — see `LocalDetectorClient.ts` for why (its own uv tool, its own
|
|
@@ -68,14 +67,24 @@ class WireError(Exception):
|
|
|
68
67
|
self.message = message
|
|
69
68
|
|
|
70
69
|
|
|
71
|
-
def parse_detect_request(raw: bytes) -> tuple[bytes, float, "
|
|
72
|
-
"""Body bytes -> (jpeg, min_confidence,
|
|
70
|
+
def parse_detect_request(raw: bytes) -> tuple[bytes, float, "list[str] | None"]:
|
|
71
|
+
"""Body bytes -> (jpeg, min_confidence, requested class names or None for all).
|
|
73
72
|
|
|
74
73
|
Every refusal here is a 400: the caller sent something this service cannot
|
|
75
74
|
act on, and saying so beats guessing. Guessing is how a profile naming a
|
|
76
75
|
class the model cannot emit returns an empty frame forever while looking
|
|
77
76
|
healthy — `yolov8_nano`, offered to ~97 of 114 profiles and loadable
|
|
78
77
|
nowhere, `vehicle_count` at ~0% fleet-wide for six months.
|
|
78
|
+
|
|
79
|
+
THE CLASS NAMES COME BACK RAW, deliberately. Which classes exist is a
|
|
80
|
+
property of the models on THIS BOX at THIS MOMENT — a specialist's ONNX
|
|
81
|
+
lands over the air and widens the vocabulary with no restart — so it cannot
|
|
82
|
+
be a constant in the wire layer, which is what `KNOWN_CLASS_NAMES` used to
|
|
83
|
+
be. `docs/runbooks/sensing-model-library.md` §3 puts class -> model routing
|
|
84
|
+
in the service "because the service is the only component that knows what is
|
|
85
|
+
resident", and the loader is the half of the service that knows. The
|
|
86
|
+
resolution and the 400 still happen BEFORE any inference; only the authority
|
|
87
|
+
moved.
|
|
79
88
|
"""
|
|
80
89
|
try:
|
|
81
90
|
payload = json.loads(raw.decode("utf-8"))
|
|
@@ -113,38 +122,7 @@ def parse_detect_request(raw: bytes) -> tuple[bytes, float, "set[str] | None"]:
|
|
|
113
122
|
raise WireError(400, "`classes` must be an array of strings")
|
|
114
123
|
if not raw_classes:
|
|
115
124
|
raise WireError(400, "`classes` was an empty array; omit the key to mean every class")
|
|
116
|
-
|
|
117
|
-
wanted, unknown = resolve_requested_classes(raw_classes)
|
|
118
|
-
# ANY unknown class is a 400, not just all of them.
|
|
119
|
-
#
|
|
120
|
-
# This used to reject only when `wanted` came back empty, so a request for
|
|
121
|
-
# ["person", "glove"] against a COCO-only model returned person detections
|
|
122
|
-
# and dropped `glove` on the floor. `resolve_requested_classes` keeps its own
|
|
123
|
-
# promise — it returns the unknowns rather than discarding them — and its
|
|
124
|
-
# docstring says exactly why: "a profile naming a class the model cannot emit
|
|
125
|
-
# is exactly the `yolov8_nano` shape". The caller then discarded them one
|
|
126
|
-
# level up, which is where the promise was actually broken.
|
|
127
|
-
#
|
|
128
|
-
# The partial case is the dangerous one and it becomes the COMMON one the
|
|
129
|
-
# moment specialist models exist: a PPE profile asks for `glove` and
|
|
130
|
-
# `hairnet` alongside `person`, and on a box holding only the general
|
|
131
|
-
# detector the operator would get person counts and silence about gloves.
|
|
132
|
-
#
|
|
133
|
-
# Refusing loses the person detections too, which is the cost. It is the
|
|
134
|
-
# right trade: which models are resident is decided upstream by the profile
|
|
135
|
-
# push, so a class the box cannot answer is a configuration error, and the
|
|
136
|
-
# client maps a 400 to `DetectorUnavailableError` -> shadow mode records
|
|
137
|
-
# `unavailable`. A wrong configuration becomes visible instead of becoming a
|
|
138
|
-
# quieter answer.
|
|
139
|
-
if unknown:
|
|
140
|
-
raise WireError(
|
|
141
|
-
400,
|
|
142
|
-
f"requested classes not in this model's vocabulary: {unknown}. "
|
|
143
|
-
f"Known classes: {sorted(KNOWN_CLASS_NAMES)}",
|
|
144
|
-
)
|
|
145
|
-
if not wanted:
|
|
146
|
-
raise WireError(400, "`classes` resolved to nothing requestable")
|
|
147
|
-
return jpeg, min_confidence, wanted
|
|
125
|
+
return jpeg, min_confidence, raw_classes
|
|
148
126
|
|
|
149
127
|
|
|
150
128
|
def _assert_wire_safe(detections: list) -> None:
|
|
@@ -177,7 +155,26 @@ def _assert_wire_safe(detections: list) -> None:
|
|
|
177
155
|
)
|
|
178
156
|
|
|
179
157
|
|
|
180
|
-
|
|
158
|
+
#: How several models' names are joined into the one `model` string the wire
|
|
159
|
+
#: carries. `LocalDetectorClient` types it `string | null` and never branches on
|
|
160
|
+
#: it (`LocalDetectorClient.ts`: `model: typeof parsed?.model === 'string' &&
|
|
161
|
+
#: parsed.model ? parsed.model : null`), so a joined string is contract-safe —
|
|
162
|
+
#: verified against the client rather than assumed. It is an ATTRIBUTION field,
|
|
163
|
+
#: and losing the second name would make a merged answer indistinguishable from
|
|
164
|
+
#: a single-model one when a specialist later turns out to be wrong.
|
|
165
|
+
MODEL_JOIN = "+"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def build_detect_response(batch) -> dict:
|
|
169
|
+
models = list(getattr(batch, "models", []) or [])
|
|
170
|
+
if not models:
|
|
171
|
+
# An unattributed result. `model` would serialise as `""` -> the client
|
|
172
|
+
# reads null, and every downstream reader loses the ability to say which
|
|
173
|
+
# artifact produced a detection. `cctv-detector-tier.md` §5 rule 2 is a
|
|
174
|
+
# whole scar about exactly that class of missing provenance, so this is
|
|
175
|
+
# a 500 rather than a plausible 200.
|
|
176
|
+
raise ValueError("batch names no model; a detection that cannot be attributed must not ship")
|
|
177
|
+
|
|
181
178
|
body = {
|
|
182
179
|
"detections": [
|
|
183
180
|
{
|
|
@@ -192,7 +189,10 @@ def build_detect_response(batch, model_name: str) -> dict:
|
|
|
192
189
|
# ~63 ms in `docs/runbooks/cctv-detector-tier.md` — a decode cost folded
|
|
193
190
|
# in here would quietly invalidate that comparison later.
|
|
194
191
|
"inference_ms": batch.inference_ms,
|
|
195
|
-
|
|
192
|
+
# THE SET THAT ANSWERED — `sensing-model-library.md` §3. One name on a
|
|
193
|
+
# single-model box, so the shipped payload is byte-identical to what it
|
|
194
|
+
# was before the library existed.
|
|
195
|
+
"model": MODEL_JOIN.join(models),
|
|
196
196
|
}
|
|
197
197
|
# A non-finite timing would serialise as a bare `NaN`, which is INVALID JSON
|
|
198
198
|
# (`json.dumps` emits it by default; `JSON.parse` refuses it). The client
|
|
@@ -272,11 +272,30 @@ class DetectorHandler(BaseHTTPRequestHandler):
|
|
|
272
272
|
loader = self.server.loader # type: ignore[attr-defined]
|
|
273
273
|
try:
|
|
274
274
|
raw = self._read_body()
|
|
275
|
-
jpeg, min_confidence,
|
|
275
|
+
jpeg, min_confidence, requested = parse_detect_request(raw)
|
|
276
276
|
except WireError as err:
|
|
277
277
|
self._json(err.status, {"error": err.message})
|
|
278
278
|
return
|
|
279
279
|
|
|
280
|
+
wanted = None
|
|
281
|
+
if requested is not None:
|
|
282
|
+
try:
|
|
283
|
+
wanted = loader.resolve_classes(requested)
|
|
284
|
+
except UnknownClasses as err:
|
|
285
|
+
# 400, BEFORE INFERENCE, on ANY unknown — not just on all of
|
|
286
|
+
# them. The partial case is the dangerous one and it becomes the
|
|
287
|
+
# COMMON one the moment specialists exist: a PPE profile asks for
|
|
288
|
+
# `glove` and `hairnet` alongside `person`, and on a box holding
|
|
289
|
+
# only the general detector two thirds of the question would
|
|
290
|
+
# vanish into a 200 that looks like an answer. Refusing costs the
|
|
291
|
+
# `person` detections too; that is the right trade, because which
|
|
292
|
+
# models are on the box was decided upstream by the profile push,
|
|
293
|
+
# so a class this box cannot answer is a configuration error and
|
|
294
|
+
# the client maps 400 to `DetectorUnavailableError`. Visible
|
|
295
|
+
# beats quieter.
|
|
296
|
+
self._json(400, {"error": err.detail or str(err)})
|
|
297
|
+
return
|
|
298
|
+
|
|
280
299
|
try:
|
|
281
300
|
batch = loader.detect(jpeg, min_confidence, wanted)
|
|
282
301
|
except DetectorNotReady as err:
|
|
@@ -290,7 +309,7 @@ class DetectorHandler(BaseHTTPRequestHandler):
|
|
|
290
309
|
return
|
|
291
310
|
|
|
292
311
|
try:
|
|
293
|
-
body = build_detect_response(batch
|
|
312
|
+
body = build_detect_response(batch)
|
|
294
313
|
except ValueError as err:
|
|
295
314
|
self._log(f"[detector] REFUSING to emit a malformed payload: {err}")
|
|
296
315
|
self._json(500, {"error": f"refused to emit a malformed payload: {err}"})
|
|
@@ -39,7 +39,9 @@ import numpy as np
|
|
|
39
39
|
import onnxruntime as ort
|
|
40
40
|
from PIL import Image
|
|
41
41
|
|
|
42
|
-
from .
|
|
42
|
+
from .detections import Detection, DetectionBatch
|
|
43
|
+
from .errors import ModelContractError
|
|
44
|
+
from .labels import ModelVocabulary
|
|
43
45
|
|
|
44
46
|
#: `rfdetr/detr.py` — ImageNet statistics, applied AFTER scaling to [0,1].
|
|
45
47
|
IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(3, 1, 1)
|
|
@@ -52,34 +54,6 @@ IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(3, 1, 1
|
|
|
52
54
|
NUM_SELECT = 300
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
@dataclass(frozen=True)
|
|
56
|
-
class Detection:
|
|
57
|
-
"""One wire-shaped detection. Pixel box, canonical label, [0,1] confidence."""
|
|
58
|
-
|
|
59
|
-
label: str
|
|
60
|
-
confidence: float
|
|
61
|
-
x: float
|
|
62
|
-
y: float
|
|
63
|
-
width: float
|
|
64
|
-
height: float
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@dataclass
|
|
68
|
-
class DetectionBatch:
|
|
69
|
-
detections: list[Detection]
|
|
70
|
-
#: The ORT forward pass alone, in ms — directly comparable to the ~63 ms in
|
|
71
|
-
#: `docs/runbooks/cctv-detector-tier.md`. Decode and post-processing are
|
|
72
|
-
#: reported separately (`total_ms`) so a slow JPEG decode cannot hide inside
|
|
73
|
-
#: a number the runbook will later be checked against.
|
|
74
|
-
inference_ms: float = 0.0
|
|
75
|
-
total_ms: float = 0.0
|
|
76
|
-
#: Boxes that survived the threshold but cover no pixels after clamping to
|
|
77
|
-
#: the frame. Counted, never silently swallowed — see `_to_wire`.
|
|
78
|
-
degenerate_dropped: int = 0
|
|
79
|
-
#: Detections whose argmax landed on one of the eleven COCO id gaps.
|
|
80
|
-
unnamed_class_dropped: int = 0
|
|
81
|
-
|
|
82
|
-
|
|
83
57
|
@dataclass
|
|
84
58
|
class GraphSpec:
|
|
85
59
|
"""What the loaded artifact actually is, read off the graph, never assumed."""
|
|
@@ -94,10 +68,6 @@ class GraphSpec:
|
|
|
94
68
|
providers: list[str] = field(default_factory=list)
|
|
95
69
|
|
|
96
70
|
|
|
97
|
-
class ModelContractError(RuntimeError):
|
|
98
|
-
"""The artifact loaded, but it is not the graph this service can label."""
|
|
99
|
-
|
|
100
|
-
|
|
101
71
|
def preprocess(jpeg: bytes, width: int, height: int) -> tuple[np.ndarray, int, int]:
|
|
102
72
|
"""JPEG bytes -> NCHW float32, plus the SUBMITTED frame's pixel size.
|
|
103
73
|
|
|
@@ -126,6 +96,8 @@ def postprocess(
|
|
|
126
96
|
frame_h: int,
|
|
127
97
|
min_confidence: float,
|
|
128
98
|
wanted: "set[str] | None" = None,
|
|
99
|
+
*,
|
|
100
|
+
vocabulary: ModelVocabulary,
|
|
129
101
|
) -> DetectionBatch:
|
|
130
102
|
"""Raw graph outputs -> wire detections, in the submitted frame's pixels.
|
|
131
103
|
|
|
@@ -134,6 +106,14 @@ def postprocess(
|
|
|
134
106
|
`wanted` filters HERE, at the service, because the alternative is shipping
|
|
135
107
|
every box across loopback to throw most away — `LocalDetectorClient`'s own
|
|
136
108
|
reasoning, and the reason `classes` rides the wire at all.
|
|
109
|
+
|
|
110
|
+
`vocabulary` IS REQUIRED AND HAS NO DEFAULT. It used to be the module-level
|
|
111
|
+
COCO-91 map, which is exactly what made a specialist inexpressible — and a
|
|
112
|
+
default naming the shipped model would be worse than the constant it
|
|
113
|
+
replaces: a PPE model would silently get COCO names for its own slots and
|
|
114
|
+
every number downstream would stay plausible. That is `cctv-detector-tier.md`
|
|
115
|
+
§5 rule 2 with the sign flipped, and it is the reason the argument is
|
|
116
|
+
keyword-only: a positional one could be filled by the wrong thing.
|
|
137
117
|
"""
|
|
138
118
|
# (1, Q, C) -> (Q, C); the graph is batch-1 by construction.
|
|
139
119
|
prob = _sigmoid(logits[0])
|
|
@@ -161,7 +141,7 @@ def postprocess(
|
|
|
161
141
|
x1 = np.clip((cx + 0.5 * w) * frame_w, 0.0, frame_w)
|
|
162
142
|
y1 = np.clip((cy + 0.5 * h) * frame_h, 0.0, frame_h)
|
|
163
143
|
|
|
164
|
-
return _to_wire(scores, class_idx, x0, y0, x1, y1, wanted)
|
|
144
|
+
return _to_wire(scores, class_idx, x0, y0, x1, y1, wanted, vocabulary)
|
|
165
145
|
|
|
166
146
|
|
|
167
147
|
def _to_wire(
|
|
@@ -172,14 +152,17 @@ def _to_wire(
|
|
|
172
152
|
x1: np.ndarray,
|
|
173
153
|
y1: np.ndarray,
|
|
174
154
|
wanted: "set[str] | None",
|
|
155
|
+
vocabulary: ModelVocabulary,
|
|
175
156
|
) -> DetectionBatch:
|
|
176
157
|
"""Assemble detections, dropping only what CANNOT be put on the wire.
|
|
177
158
|
|
|
178
159
|
Two drops, and each is counted rather than swallowed:
|
|
179
160
|
|
|
180
|
-
* an UNNAMED CLASS SLOT.
|
|
181
|
-
with no name
|
|
182
|
-
|
|
161
|
+
* an UNNAMED CLASS SLOT. For COCO-91, eleven of the ninety-one slots are
|
|
162
|
+
category-id gaps with no name; a specialist may have its own. The name
|
|
163
|
+
comes from THIS MODEL's committed vocabulary and from nowhere else.
|
|
164
|
+
`docs/runbooks/cctv-detector-tier.md` §5 rule 2 forbids guessing; a
|
|
165
|
+
detection with no nameable class is dropped.
|
|
183
166
|
* a ZERO-EXTENT BOX. A box clamped flush against a frame edge can have zero
|
|
184
167
|
width or height, and `LocalDetectorClient` REJECTS THE ENTIRE PAYLOAD on
|
|
185
168
|
one — correctly, since it cannot tell a schema-drifted service from a
|
|
@@ -197,8 +180,9 @@ def _to_wire(
|
|
|
197
180
|
degenerate = 0
|
|
198
181
|
unnamed = 0
|
|
199
182
|
|
|
183
|
+
slot_names = vocabulary.slot_names
|
|
200
184
|
for i in range(scores.shape[0]):
|
|
201
|
-
name =
|
|
185
|
+
name = slot_names.get(int(class_idx[i]))
|
|
202
186
|
if name is None:
|
|
203
187
|
unnamed += 1
|
|
204
188
|
continue
|
|
@@ -221,7 +205,12 @@ def _to_wire(
|
|
|
221
205
|
# sigmoid is (0,1); clamp only guards float64 round-trip at the edges.
|
|
222
206
|
out.append(Detection(name, min(1.0, max(0.0, conf)), left, top, width, height))
|
|
223
207
|
|
|
224
|
-
return DetectionBatch(
|
|
208
|
+
return DetectionBatch(
|
|
209
|
+
out,
|
|
210
|
+
degenerate_dropped=degenerate,
|
|
211
|
+
unnamed_class_dropped=unnamed,
|
|
212
|
+
models=[vocabulary.model_name],
|
|
213
|
+
)
|
|
225
214
|
|
|
226
215
|
|
|
227
216
|
def _sigmoid(x: np.ndarray) -> np.ndarray:
|
|
@@ -250,21 +239,30 @@ class OnnxDetector:
|
|
|
250
239
|
overrides them. Pinning them is the mistake the same section names.
|
|
251
240
|
"""
|
|
252
241
|
|
|
253
|
-
def __init__(
|
|
242
|
+
def __init__(
|
|
243
|
+
self,
|
|
244
|
+
model_path: Path,
|
|
245
|
+
vocabulary: ModelVocabulary,
|
|
246
|
+
intra_op_threads: "int | None" = None,
|
|
247
|
+
):
|
|
254
248
|
self.model_path = model_path
|
|
255
|
-
self.
|
|
249
|
+
self.vocabulary = vocabulary
|
|
250
|
+
self.model_name = vocabulary.model_name
|
|
256
251
|
self._lock = threading.Lock()
|
|
257
252
|
|
|
258
253
|
opts = ort.SessionOptions()
|
|
259
254
|
if intra_op_threads is not None:
|
|
260
255
|
opts.intra_op_num_threads = intra_op_threads
|
|
261
|
-
# Loopback service, one process
|
|
256
|
+
# Loopback service, one process: log only what a fault needs. Several
|
|
257
|
+
# sessions may be resident at once now (`registry.py`), which is exactly
|
|
258
|
+
# why the thread counts below stay at ORT's default — pinning them would
|
|
259
|
+
# multiply an already non-monotonic pool by the number of models.
|
|
262
260
|
opts.log_severity_level = 3
|
|
263
261
|
|
|
264
262
|
self._session = ort.InferenceSession(
|
|
265
263
|
str(model_path), sess_options=opts, providers=["CPUExecutionProvider"]
|
|
266
264
|
)
|
|
267
|
-
self.spec = _read_graph_spec(self._session)
|
|
265
|
+
self.spec = _read_graph_spec(self._session, vocabulary)
|
|
268
266
|
|
|
269
267
|
def detect(
|
|
270
268
|
self,
|
|
@@ -282,21 +280,38 @@ class OnnxDetector:
|
|
|
282
280
|
)
|
|
283
281
|
infer_ms = (time.perf_counter() - infer_started) * 1000.0
|
|
284
282
|
|
|
285
|
-
batch = postprocess(
|
|
283
|
+
batch = postprocess(
|
|
284
|
+
outputs[0],
|
|
285
|
+
outputs[1],
|
|
286
|
+
frame_w,
|
|
287
|
+
frame_h,
|
|
288
|
+
min_confidence,
|
|
289
|
+
wanted,
|
|
290
|
+
vocabulary=self.vocabulary,
|
|
291
|
+
)
|
|
286
292
|
batch.inference_ms = infer_ms
|
|
287
293
|
batch.total_ms = (time.perf_counter() - started) * 1000.0
|
|
288
294
|
return batch
|
|
289
295
|
|
|
290
296
|
|
|
291
|
-
def _read_graph_spec(
|
|
297
|
+
def _read_graph_spec(
|
|
298
|
+
session: "ort.InferenceSession",
|
|
299
|
+
vocabulary: ModelVocabulary,
|
|
300
|
+
) -> GraphSpec:
|
|
292
301
|
"""Read the contract off the loaded graph, and REFUSE a graph we cannot label.
|
|
293
302
|
|
|
294
|
-
The exported artifact carries no class metadata, so the
|
|
295
|
-
`labels.py` is the only thing naming its
|
|
296
|
-
is not
|
|
297
|
-
|
|
298
|
-
is the `yolov8_nano` failure with
|
|
299
|
-
|
|
303
|
+
The exported artifact carries no class metadata, so the committed
|
|
304
|
+
vocabulary in `labels.py` is the only thing naming its slots. If the graph's
|
|
305
|
+
class axis is not the width that vocabulary declares, it does not describe
|
|
306
|
+
this artifact — and applying it anyway renames every object in the venue
|
|
307
|
+
while every number stays plausible. That is the `yolov8_nano` failure with
|
|
308
|
+
the sign flipped: instead of a model offered and never loadable, a model
|
|
309
|
+
loaded and never right.
|
|
310
|
+
|
|
311
|
+
THE CHECK IS PER MODEL, NOT PER SERVICE. With a library, "91 slots" is one
|
|
312
|
+
model's fact rather than the box's, so the width is read off the vocabulary
|
|
313
|
+
being loaded. A specialist with a 4-wide axis loads happily against ITS OWN
|
|
314
|
+
vocabulary and is still refused against anyone else's.
|
|
300
315
|
|
|
301
316
|
So a mismatch raises, the service goes UNHEALTHY with the reason, and
|
|
302
317
|
nothing is emitted. The 2.1%-of-events figure the placement decision rests
|
|
@@ -335,11 +350,11 @@ def _read_graph_spec(session: "ort.InferenceSession") -> GraphSpec:
|
|
|
335
350
|
raise ModelContractError(f"`labels` is not (N, Q, C): {logit_shape}")
|
|
336
351
|
|
|
337
352
|
num_classes = logit_shape[2]
|
|
338
|
-
if num_classes !=
|
|
353
|
+
if num_classes != vocabulary.logit_slots:
|
|
339
354
|
raise ModelContractError(
|
|
340
|
-
f"model has {num_classes} class slots; the committed
|
|
341
|
-
f"{
|
|
342
|
-
"index
|
|
355
|
+
f"model has {num_classes} class slots; the committed vocabulary for "
|
|
356
|
+
f"{vocabulary.model_name} describes {vocabulary.logit_slots}. Refusing rather "
|
|
357
|
+
"than labelling boxes by an index that vocabulary does not name — see labels.py."
|
|
343
358
|
)
|
|
344
359
|
|
|
345
360
|
return GraphSpec(
|
|
@@ -352,3 +367,17 @@ def _read_graph_spec(session: "ort.InferenceSession") -> GraphSpec:
|
|
|
352
367
|
num_classes=int(num_classes),
|
|
353
368
|
providers=list(session.get_providers()),
|
|
354
369
|
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
#: Re-exported so `from .session import DetectionBatch` keeps working. ONE
|
|
373
|
+
#: definition, in `detections.py`, which is stdlib-only so `registry.py` can
|
|
374
|
+
#: merge two models' answers on a runner that has no onnxruntime.
|
|
375
|
+
__all__ = [
|
|
376
|
+
"Detection",
|
|
377
|
+
"DetectionBatch",
|
|
378
|
+
"GraphSpec",
|
|
379
|
+
"ModelContractError",
|
|
380
|
+
"OnnxDetector",
|
|
381
|
+
"postprocess",
|
|
382
|
+
"preprocess",
|
|
383
|
+
]
|
package/dist/EdgeAgent.js
CHANGED
|
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
|
|
|
44
44
|
const CommandRouter_1 = require("./CommandRouter");
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const dataDir_1 = require("./dataDir");
|
|
47
|
-
const SDK_VERSION = '0.2.
|
|
47
|
+
const SDK_VERSION = '0.2.82';
|
|
48
48
|
class EdgeAgent extends events_1.EventEmitter {
|
|
49
49
|
constructor(config, deps) {
|
|
50
50
|
super();
|
package/dist/diagnose.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface DiagnosticReport {
|
|
|
31
31
|
skipped: number;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
declare const SDK_VERSION = "0.2.
|
|
34
|
+
declare const SDK_VERSION = "0.2.82";
|
|
35
35
|
declare function checkNodeVersion(): CheckResult;
|
|
36
36
|
declare function checkPlatform(): CheckResult;
|
|
37
37
|
declare function checkMemory(): CheckResult;
|
package/dist/diagnose.js
CHANGED
|
@@ -63,7 +63,7 @@ const localNetworkAccess_1 = require("./localNetworkAccess");
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
// SDK version — mirrors package.json; kept in sync by CI
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
|
-
const SDK_VERSION = '0.2.
|
|
66
|
+
const SDK_VERSION = '0.2.82';
|
|
67
67
|
exports._SDK_VERSION = SDK_VERSION;
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
// ANSI helpers (no external deps)
|