@trillboards/edge-sdk 0.2.95 → 0.2.96
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/dist/DetectorFieldProducer.d.ts +482 -0
- package/dist/DetectorFieldProducer.d.ts.map +1 -0
- package/dist/DetectorFieldProducer.js +691 -0
- package/dist/DetectorFieldProducer.js.map +1 -0
- package/dist/EdgeAgent.js +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DetectorFieldProducer — the connector's detector→declared-field producer.
|
|
3
|
+
*
|
|
4
|
+
* ── THE GAP THIS CLOSES ────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The Android agent has had one of these for a year: `QueueEstimationProcessor`
|
|
7
|
+
* turns EfficientDet detections into `pedestrian_count` / `queue_length` /
|
|
8
|
+
* `queue_detected`, keyed under the model that produced them
|
|
9
|
+
* (`AudienceSensingService.kt:3090` — `model_outputs.efficientdet.{…}`), and
|
|
10
|
+
* those are exactly the fields `efficientdet` declares in its registry
|
|
11
|
+
* `outputs_schema`.
|
|
12
|
+
*
|
|
13
|
+
* The connector has had none. It runs a real detector on 127.0.0.1:9380 at
|
|
14
|
+
* 15–90 ms a frame, and the only thing that consumes the result is
|
|
15
|
+
* `DetectorShadowProbe`, which collapses the whole detection set to a
|
|
16
|
+
* three-valued enum and discards the boxes. So "how many people are there" —
|
|
17
|
+
* a question the detector answers in milliseconds, with boxes — was answered by
|
|
18
|
+
* asking a language model in prose at ~5,645 ms, at ~9% precision on specifics
|
|
19
|
+
* (docs/runbooks/sensing-catch-rate.md §4a).
|
|
20
|
+
*
|
|
21
|
+
* ── ONE FIELD, ONE PRODUCER ────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* The input is the `producer` half of `modelClassLiveness.partitionProfileFields`
|
|
24
|
+
* — `{field, modelId, type}` per entry, where `modelId` is the model whose
|
|
25
|
+
* registry row DECLARES that field as an output. Ownership is data; this file is
|
|
26
|
+
* only the resolution. Nothing here decides which model answers a field, and it
|
|
27
|
+
* must never acquire that opinion: a second place that can decide is how one
|
|
28
|
+
* fact ends up with two producers, which is the defect the partition exists to
|
|
29
|
+
* remove.
|
|
30
|
+
*
|
|
31
|
+
* The output is TOTAL: `produced.length + withheld.length === fields.length`,
|
|
32
|
+
* always, on every path including the ones where the detector never answered.
|
|
33
|
+
* Same contract, and the same reason, as the partition's own
|
|
34
|
+
* `producer ∪ vlm ∪ withheld === fields`: two independent filters that must
|
|
35
|
+
* agree is precisely how a field goes missing with nothing to point at.
|
|
36
|
+
*
|
|
37
|
+
* This is also why the input is the PARTITION and not the profile's raw
|
|
38
|
+
* `fields[]`. `person_count` is already in `localInferenceSchema`'s
|
|
39
|
+
* `PRESENCE_PRIMITIVE_ORDER`, so every profile asks the language model for it
|
|
40
|
+
* today; a producer handed the raw list would answer it as well and the reading
|
|
41
|
+
* an operator saw would depend on which component wrote last. The partition is
|
|
42
|
+
* what takes a produced field OUT of the prompt, and a producer that could not
|
|
43
|
+
* be told which fields those are could not be correct.
|
|
44
|
+
*
|
|
45
|
+
* ── A GRANTED MODEL WHOSE BYTES NEVER LANDED YIELDS A *MISSING* FIELD ──────
|
|
46
|
+
*
|
|
47
|
+
* A model can be granted by the profile and still be absent from the box: the
|
|
48
|
+
* grant comes from `GET /v2/earner/cameras/sensing-profile` and the artifact
|
|
49
|
+
* arrives separately over `/v1/federated/models`, which was 404 for sixteen days
|
|
50
|
+
* in August.
|
|
51
|
+
*
|
|
52
|
+
* THAT HAS TWO DOORS, and only the first one looks like a failure:
|
|
53
|
+
*
|
|
54
|
+
* the class is unknown to the box — no resident model can emit it, so the
|
|
55
|
+
* service answers **400 before inference** (`server.py::_detect`,
|
|
56
|
+
* deliberately, on ANY unknown class) and `LocalDetectorClient` raises
|
|
57
|
+
* `DetectorUnavailableError`.
|
|
58
|
+
*
|
|
59
|
+
* the class is owned by SOMEBODY ELSE — a different resident model declares
|
|
60
|
+
* it, so the call **succeeds with a 200** and a perfectly good answer from
|
|
61
|
+
* the wrong instrument. Overlap is the norm: `hardhat` is declared by both
|
|
62
|
+
* `ppe_vyra_14cls_v8m` and `ppe_construction_17cls_v8n`, `person` by those
|
|
63
|
+
* two plus `rfdetr_medium`. The attribution check in `produce()` is the only
|
|
64
|
+
* thing that closes this one — see the long note at its call site before
|
|
65
|
+
* deciding it is redundant with the 400.
|
|
66
|
+
*
|
|
67
|
+
* In BOTH the field is then ABSENT. Not `false`, not `0`, and — the important
|
|
68
|
+
* one — NOT handed to the language model instead.
|
|
69
|
+
* `routeEvaluatorService.js:1272` already gets this right one layer up: a
|
|
70
|
+
* focused route reads only its mapped field and yields `no_field` rather than
|
|
71
|
+
* falling back. Substituting a different producer
|
|
72
|
+
* mid-flight is the split-brain again, in TIME instead of space: the same
|
|
73
|
+
* question, answered by two different instruments on two different frames, with
|
|
74
|
+
* nothing on the reading saying which.
|
|
75
|
+
*
|
|
76
|
+
* ── `unavailable` NEVER COLLAPSES INTO `empty` ─────────────────────────────
|
|
77
|
+
*
|
|
78
|
+
* Structural, not a convention: a value is written in exactly one place in this
|
|
79
|
+
* file, from a count derived from a parsed 2xx. There is no path from a caught
|
|
80
|
+
* error to a `false` or a `0`. Every failure — transport, 400, 503, a 2xx whose
|
|
81
|
+
* envelope is not the contract, the budget running out — lands in `withheld`.
|
|
82
|
+
*
|
|
83
|
+
* `LocalDetectorClient` and `DetectorShadowProbe` both carry this distinction in
|
|
84
|
+
* their own headers, and it is the same one: a cascade that reads "detector
|
|
85
|
+
* down" as "nobody there" suppresses every alert while `postCount`,
|
|
86
|
+
* `sensingState:'active'` and the portal's "AI active" tile all stay green
|
|
87
|
+
* (docs/runbooks/cctv-connector-runtime-edge-cases.md).
|
|
88
|
+
*
|
|
89
|
+
* ── ONE CALL PER MODEL, NOT ONE CALL PER FRAME ─────────────────────────────
|
|
90
|
+
*
|
|
91
|
+
* The service refuses a request naming ANY class it cannot resolve — it does not
|
|
92
|
+
* answer the part it recognises — and its own comment says why: "Refusing costs
|
|
93
|
+
* the `person` detections too; that is the right trade." That is correct for ONE
|
|
94
|
+
* question. A frame's declared fields are N INDEPENDENT questions, and unioning
|
|
95
|
+
* them into a single call would make them share a fate they do not share: one
|
|
96
|
+
* specialist whose bytes never landed would take `person_count` down with it.
|
|
97
|
+
*
|
|
98
|
+
* So the classes are grouped by the `modelId` the partition already supplies,
|
|
99
|
+
* one call per model, each carrying the union of the classes THAT model's fields
|
|
100
|
+
* need. A model that cannot answer loses its own fields and no others.
|
|
101
|
+
*
|
|
102
|
+
* This is not the connector holding a class→model catalogue. It cannot and must
|
|
103
|
+
* not: `docs/runbooks/sensing-model-library.md` is explicit that the service
|
|
104
|
+
* owns class→model routing because it is the only component that knows what is
|
|
105
|
+
* resident, and the wire carries `classes` and no model selector. The grouping
|
|
106
|
+
* here is FAILURE ISOLATION over an ownership fact the server sent us.
|
|
107
|
+
*
|
|
108
|
+
* Calls run SEQUENTIALLY. The box has one inference slot; issuing them in
|
|
109
|
+
* parallel queues them inside the service and spends the budget on scheduling.
|
|
110
|
+
*
|
|
111
|
+
* @reuse-audit
|
|
112
|
+
* considered: services/regression/detectorCorpusEndpoint.js
|
|
113
|
+
* - reused, as the SEMANTICS this file is a second implementation of: the
|
|
114
|
+
* `<concept>_present` / `<concept>_count` key shape, `toDetectorClassName`
|
|
115
|
+
* byte-for-byte, and the "trim each class at its own threshold, throw on a
|
|
116
|
+
* label nobody asked for" rule, comments included. NOT imported: that is
|
|
117
|
+
* CommonJS inside the API image and this is the connector's TypeScript
|
|
118
|
+
* bundle; the API does not ship edge-sdk and edge-sdk does not ship the
|
|
119
|
+
* API. What keeps them from drifting is that they answer the same corpus —
|
|
120
|
+
* the six live `expected_output` keys (`person_count`, `person_present`,
|
|
121
|
+
* `forklift_present`, `hardhat_present`, `excavators_present`,
|
|
122
|
+
* `fire_extinguisher_present`) ARE the field names, and
|
|
123
|
+
* `__tests__/DetectorFieldProducer.test.ts` pins this side onto the classes
|
|
124
|
+
* those models declare in `coco_classes`.
|
|
125
|
+
* - ONE DELIBERATE DIVERGENCE, marked at `countsForClasses`: its single-class
|
|
126
|
+
* branch returns `detections.length` unread, and this one reads the label
|
|
127
|
+
* on every branch. That is not a port that drifted — it is the same
|
|
128
|
+
* principle under a different consumer. See the note there before
|
|
129
|
+
* "restoring" it.
|
|
130
|
+
* considered: trillboard-ctv/.../audience/QueueEstimationProcessor.kt
|
|
131
|
+
* - reused: its SHAPE. A processor that turns one frame's detection set into
|
|
132
|
+
* a map keyed by registry-canonical snake_case field names, with the
|
|
133
|
+
* producing model's id travelling ON the values (`model_outputs.<modelId>`)
|
|
134
|
+
* so a reading can be attributed. `toJson`'s note — "keys use
|
|
135
|
+
* registry-canonical snake_case so `chipPersistence.sanitizeFieldKey` is
|
|
136
|
+
* idempotent on canonical input" — is why `produced[].field` is the
|
|
137
|
+
* DECLARED name verbatim and is never re-spelled here.
|
|
138
|
+
* - deliberately NOT ported: its temporal half — `serviceRatePerMinute`,
|
|
139
|
+
* `estimatedWaitTimeMinutes`, `trend`, and the 30-snapshot history they
|
|
140
|
+
* are derived from. Nothing in any producer half declares those fields, so
|
|
141
|
+
* emitting them would advertise a capability no registry row owns — the
|
|
142
|
+
* `yolov8_nano` shape, where a model was offered to ~97 of 114 profile
|
|
143
|
+
* authors while every device hardcoded it unloadable. When a model DOES
|
|
144
|
+
* declare `queue_length`, that is a field with an owner and it arrives
|
|
145
|
+
* here as an ordinary producer entry.
|
|
146
|
+
* considered: edge-sensing/src/processors/FaceDetector.ts — `nonMaxSuppression`
|
|
147
|
+
* + `iou` (the kernel `LocalDetectorClient`'s header names as the one to use)
|
|
148
|
+
* - deliberately NOT used, and this is the one worth reading twice. The
|
|
149
|
+
* SERVICE already ran NMS: `deploy/detector/trillboards_detector/decode.py`
|
|
150
|
+
* does class-offset greedy NMS at `NMS_IOU_THRESHOLD = 0.7` on the anchor
|
|
151
|
+
* branch, transcribed from ultralytics, and deliberately runs NONE on the
|
|
152
|
+
* DETR branch because "running NMS over these rows would be a second
|
|
153
|
+
* suppression the model was neither trained nor scored with". A pass here
|
|
154
|
+
* would be exactly that second suppression, on both branches, and it would
|
|
155
|
+
* move the counts away from the ones the committed score cards fitted their
|
|
156
|
+
* thresholds against. The kernel is fine; a fourth suppression is not.
|
|
157
|
+
* considered: edge-core `CentroidTracker` (the shared nearest-centroid tracker)
|
|
158
|
+
* - deliberately NOT used: no field in any producer half needs cross-frame
|
|
159
|
+
* identity. `<concept>_present` and `<concept>_count` are both properties
|
|
160
|
+
* of ONE frame — which is also how the Android processor computes
|
|
161
|
+
* `estimatedQueueLength` (`current.faceCount`, the latest snapshot). Wiring
|
|
162
|
+
* a tracker in for fields that do not ask for one would add per-feed state
|
|
163
|
+
* with no consumer. When dwell or a unique count lands, that tracker is the
|
|
164
|
+
* one to call — it is already the consolidation of three, and a fourth is
|
|
165
|
+
* what its own header exists to prevent.
|
|
166
|
+
* considered: this package's `zoneContainment.countInZone`
|
|
167
|
+
* - not used HERE: it takes a frame size and a normalized rect, and with a
|
|
168
|
+
* null rect it IS `boxes.length`. A field focused to an operator's drawn
|
|
169
|
+
* region is the natural next consumer of both files at once — the count
|
|
170
|
+
* becomes `countInZone(boxes, frame, rect)` and nothing else changes — but
|
|
171
|
+
* a producer entry carries no region today and inventing one would be a
|
|
172
|
+
* second home for a fact `sensing_route_screens` already owns.
|
|
173
|
+
* considered: `DetectorShadowProbe`
|
|
174
|
+
* - not extended, and not called: it is a MEASUREMENT that must change
|
|
175
|
+
* nothing, with a backoff and a 15 s per-feed cadence bound tuned to that
|
|
176
|
+
* job. A producer answering an operator's declared field is a PRODUCT path
|
|
177
|
+
* with the opposite cadence obligation.
|
|
178
|
+
* considered: edge-core `ModelManager.hasModel(modelId)` — the connector's own
|
|
179
|
+
* "is this artifact on disk" check (`fs.existsSync` over the registered path)
|
|
180
|
+
* - deliberately NOT used as the arrival gate. It answers about the
|
|
181
|
+
* CONNECTOR's download directory; what decides whether a class can be
|
|
182
|
+
* answered is the SERVICE's `ClassIndex`, which `labels.py` builds only
|
|
183
|
+
* over models whose artifact is on the box. Those are two opinions about
|
|
184
|
+
* one fact and they can disagree — a file present but the wrong graph
|
|
185
|
+
* (`ModelContractError`), or resident in a service started before the
|
|
186
|
+
* download. Pre-checking here would also mean a `hasModel` that returned
|
|
187
|
+
* true still has to handle the 400, so the check would be advisory at best
|
|
188
|
+
* and a second catalogue at worst. The 400 IS the arrival check, from the
|
|
189
|
+
* only component that knows.
|
|
190
|
+
*/
|
|
191
|
+
import { LocalDetectorClient } from './LocalDetectorClient';
|
|
192
|
+
/**
|
|
193
|
+
* One entry of the `producer` half of a field partition.
|
|
194
|
+
*
|
|
195
|
+
* Mirrors what `modelClassLiveness.partitionProfileFields` puts in `producer[]`:
|
|
196
|
+
* the profile's own field entry, plus the `modelId` that owns it and the type
|
|
197
|
+
* the owning registry row declared.
|
|
198
|
+
*
|
|
199
|
+
* `type` is `string | null` rather than `WireSensingProfileFieldType` ON PURPOSE.
|
|
200
|
+
* The profile's `fields[].type` is that closed enum, but a producer entry's type
|
|
201
|
+
* comes from `sensing_model_registry.outputs_schema[].type`, which nothing
|
|
202
|
+
* validates on write — `declaredOutputFields` reads it as `o.type ?? null` and
|
|
203
|
+
* says so. Declaring it narrower here would be this file asserting a contract
|
|
204
|
+
* the data does not have.
|
|
205
|
+
*/
|
|
206
|
+
export interface ProducerField {
|
|
207
|
+
/** The DECLARED field name, verbatim. Never re-spelled — see the reuse audit. */
|
|
208
|
+
field: string;
|
|
209
|
+
/** `sensing_model_registry.model_id` of the model that declares this field. */
|
|
210
|
+
modelId: string;
|
|
211
|
+
/** The declared type, or null when the owning row carried none. */
|
|
212
|
+
type?: string | null;
|
|
213
|
+
}
|
|
214
|
+
/** What a detector can produce: a presence boolean or an instance count. */
|
|
215
|
+
export type ProducedFieldValue = boolean | number;
|
|
216
|
+
/**
|
|
217
|
+
* One field this frame answered, with everything needed to attribute it.
|
|
218
|
+
*
|
|
219
|
+
* `minConfidence` travels WITH the value rather than being restated by whoever
|
|
220
|
+
* reads it, for the reason `DetectorShadowProbe.minConfidence` does: a fleet
|
|
221
|
+
* mid-upgrade runs two operating points at once, and pooling readings taken at
|
|
222
|
+
* two thresholds averages two different classifiers with nothing saying so.
|
|
223
|
+
*/
|
|
224
|
+
export interface ProducedField {
|
|
225
|
+
field: string;
|
|
226
|
+
value: ProducedFieldValue;
|
|
227
|
+
/** The model the PARTITION said owns this field. */
|
|
228
|
+
modelId: string;
|
|
229
|
+
/** The class asked for, in the service's wire spelling. */
|
|
230
|
+
className: string;
|
|
231
|
+
/** The confidence floor this value was counted at. */
|
|
232
|
+
minConfidence: number;
|
|
233
|
+
/** The artifact that actually answered, as it names itself. Null if it did not say. */
|
|
234
|
+
detectorModel: string | null;
|
|
235
|
+
}
|
|
236
|
+
/** Why a declared field has no reading this frame. */
|
|
237
|
+
export declare const PRODUCER_WITHHOLD_REASONS: {
|
|
238
|
+
/**
|
|
239
|
+
* The field name is not `<concept>_present` / `<concept>_count`, so no
|
|
240
|
+
* detector class can be derived from it.
|
|
241
|
+
*
|
|
242
|
+
* WITHHELD RATHER THAN DROPPED. A registry row may declare `queue_length` or
|
|
243
|
+
* `vehicle_trend` — `efficientdet` declares both — and a detection set answers
|
|
244
|
+
* neither. Guessing a class out of `trend` is how a bus gets counted as a
|
|
245
|
+
* person; silently dropping it is how a field goes missing with nothing to
|
|
246
|
+
* point at.
|
|
247
|
+
*/
|
|
248
|
+
readonly UNMAPPED: "unmapped";
|
|
249
|
+
/**
|
|
250
|
+
* The declared type contradicts what the name asks for — a `_present` field
|
|
251
|
+
* declared `counter`, say.
|
|
252
|
+
*
|
|
253
|
+
* The suffix decides the COMPUTATION and the type is the published contract;
|
|
254
|
+
* when they disagree the row is defective and either answer is wrong. Refusing
|
|
255
|
+
* names the defect instead of writing a boolean into a slot rendered as a
|
|
256
|
+
* number.
|
|
257
|
+
*/
|
|
258
|
+
readonly TYPE_CONTRADICTS_NAME: "type_contradicts_name";
|
|
259
|
+
/**
|
|
260
|
+
* The detector did not answer for this field's model.
|
|
261
|
+
*
|
|
262
|
+
* Transport, 400 (a class no resident model can emit — i.e. a granted model
|
|
263
|
+
* whose bytes never landed), 503, a 2xx whose envelope is not the contract, a
|
|
264
|
+
* response labelled with a class nobody asked for, or the budget running out.
|
|
265
|
+
* All of them mean the same thing to a consumer: there is NO READING. None of
|
|
266
|
+
* them may become a value.
|
|
267
|
+
*/
|
|
268
|
+
readonly DETECTOR_UNAVAILABLE: "detector_unavailable";
|
|
269
|
+
/**
|
|
270
|
+
* The entry names no model.
|
|
271
|
+
*
|
|
272
|
+
* Its own reason rather than `unmapped`, because it is a defect in a different
|
|
273
|
+
* place: the NAME was readable and it is the OWNERSHIP that is missing. The
|
|
274
|
+
* partition cannot emit such an entry — it only puts a field in `producer[]`
|
|
275
|
+
* once it has found an owner — so this is a claim about a caller, and saying
|
|
276
|
+
* "no class follows from that name" about it would send the reader to the
|
|
277
|
+
* registry's `outputs_schema` when the fault is upstream of it.
|
|
278
|
+
*
|
|
279
|
+
* It is refused rather than answered anonymously. The class list would resolve
|
|
280
|
+
* fine and the count would be correct; what would be lost is attribution, and
|
|
281
|
+
* "a detection whose producer cannot be named is a detection that cannot be
|
|
282
|
+
* attributed when a specialist later turns out to be wrong"
|
|
283
|
+
* (`deploy/detector/trillboards_detector/detections.py`).
|
|
284
|
+
*/
|
|
285
|
+
readonly UNOWNED: "unowned";
|
|
286
|
+
/**
|
|
287
|
+
* The same field name appeared twice in the producer half.
|
|
288
|
+
*
|
|
289
|
+
* The first occurrence is answered and the rest are withheld, so the total
|
|
290
|
+
* contract holds over the input array exactly. A repeated field is the "one
|
|
291
|
+
* fact, two answers" defect at its smallest scale and it is worth a name.
|
|
292
|
+
*/
|
|
293
|
+
readonly DUPLICATE: "duplicate";
|
|
294
|
+
};
|
|
295
|
+
export type ProducerWithholdReason = (typeof PRODUCER_WITHHOLD_REASONS)[keyof typeof PRODUCER_WITHHOLD_REASONS];
|
|
296
|
+
export interface WithheldProducerField {
|
|
297
|
+
field: string;
|
|
298
|
+
modelId: string;
|
|
299
|
+
reason: ProducerWithholdReason;
|
|
300
|
+
detail: string;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* One frame's answer.
|
|
304
|
+
*
|
|
305
|
+
* `produced.length + withheld.length === fields.length`. Always.
|
|
306
|
+
*/
|
|
307
|
+
export interface ProducedFieldSet {
|
|
308
|
+
produced: ProducedField[];
|
|
309
|
+
withheld: WithheldProducerField[];
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The slice of a detector this producer uses.
|
|
313
|
+
*
|
|
314
|
+
* Picked off `LocalDetectorClient` itself rather than re-declared, so a change
|
|
315
|
+
* to `detect`'s signature breaks here too — and narrowed to the one method,
|
|
316
|
+
* because this producer never probes health. `DetectorShadowProbe` owns liveness
|
|
317
|
+
* and backoff; a producer that also had an opinion about when the service is up
|
|
318
|
+
* would be a second such opinion, and requiring an `isHealthy` stub in every
|
|
319
|
+
* test would imply one exists.
|
|
320
|
+
*/
|
|
321
|
+
export type DetectorFieldClient = Pick<LocalDetectorClient, 'detect'>;
|
|
322
|
+
/** Builds a client for ONE model group's classes and operating point. */
|
|
323
|
+
export type DetectorClientFactory = (options: {
|
|
324
|
+
classes: string[];
|
|
325
|
+
minConfidence: number;
|
|
326
|
+
timeoutMs: number;
|
|
327
|
+
}) => DetectorFieldClient;
|
|
328
|
+
/**
|
|
329
|
+
* The device's operating point when a model measured none of its own. 0.25.
|
|
330
|
+
*
|
|
331
|
+
* `LocalDetectorClient`'s default, and a FALLBACK rather than an answer — the
|
|
332
|
+
* same constant and the same caveat as
|
|
333
|
+
* `detectorCorpusEndpoint.DEFAULT_MIN_CONFIDENCE`. A model whose score card
|
|
334
|
+
* SELECTED a threshold must be counted at THAT threshold or the field is
|
|
335
|
+
* produced by a classifier no committed measurement describes: `gh_forklift_combine`
|
|
336
|
+
* fits 0.7435 and `rfdetr_medium`'s person gate 0.2939, and at 0.25 the
|
|
337
|
+
* published precision/recall simply do not apply to what shipped.
|
|
338
|
+
*/
|
|
339
|
+
export declare const DEFAULT_PRODUCER_MIN_CONFIDENCE = 0.25;
|
|
340
|
+
/**
|
|
341
|
+
* Per-call transport timeout. 1000 ms, matching `DEFAULT_SHADOW_TIMEOUT_MS`.
|
|
342
|
+
*
|
|
343
|
+
* NOT `LocalDetectorClient`'s own 5 s default: this runs on the path to an
|
|
344
|
+
* analysis, and the detector's measured cost is ~63 ms p50 / ~79 ms p95, so
|
|
345
|
+
* 1000 ms is >12x headroom for the slowest plausible answer.
|
|
346
|
+
*/
|
|
347
|
+
export declare const DEFAULT_PRODUCER_TIMEOUT_MS = 1000;
|
|
348
|
+
/**
|
|
349
|
+
* TOTAL budget across every model call for one frame. 1500 ms.
|
|
350
|
+
*
|
|
351
|
+
* A total and not a per-call bound, because the question it answers is "how long
|
|
352
|
+
* may producing this frame's fields hold the tick", and that is a property of
|
|
353
|
+
* the frame, not of one model. At the measured p95 a four-model portfolio costs
|
|
354
|
+
* ~316 ms, so this is ~4.7x headroom and cannot bind on a healthy box.
|
|
355
|
+
*
|
|
356
|
+
* When it DOES bind, the models that never got their call are withheld
|
|
357
|
+
* `detector_unavailable` — a missing field, which is the same discipline the
|
|
358
|
+
* 400 case gets and for the same reason. A budget that returned a value it did
|
|
359
|
+
* not measure would be worse than one that stalls.
|
|
360
|
+
*
|
|
361
|
+
* It bounds the number of calls STARTED and, via the per-call timeout below,
|
|
362
|
+
* how long each may sit idle. It does not close the one hole
|
|
363
|
+
* `LocalDetectorClient`'s header names — a server dribbling bytes slowly under a
|
|
364
|
+
* large Content-Length keeps the socket busy so the inactivity timer never fires
|
|
365
|
+
* — because that hole is in the transport and belongs to every client in this
|
|
366
|
+
* package; two call-site races for it already exist and a third is not the fix.
|
|
367
|
+
*/
|
|
368
|
+
export declare const DEFAULT_PRODUCE_DEADLINE_MS = 1500;
|
|
369
|
+
/**
|
|
370
|
+
* The detector's canonical wire spelling.
|
|
371
|
+
*
|
|
372
|
+
* `normalize_class_name` in
|
|
373
|
+
* `deploy/detector/trillboards_detector/labels.py` is
|
|
374
|
+
* `name.strip().lower().replace("_", " ").replace("-", " ")`, and `labels.py`
|
|
375
|
+
* REFUSES to import a vocabulary whose names are not already folded — so this is
|
|
376
|
+
* the only spelling `/detect` resolves. A field carries its concept in the
|
|
377
|
+
* spelling a JSON key can hold (`fire_extinguisher`); the service is asked in
|
|
378
|
+
* the spelling it demands (`fire extinguisher`). One fold, one place.
|
|
379
|
+
*
|
|
380
|
+
* Byte-identical to `detectorCorpusEndpoint.toDetectorClassName`, deliberately:
|
|
381
|
+
* the corpus that GATES a model's rollout and the producer that READS it in the
|
|
382
|
+
* field must ask for the same class, or the thing measured is not the thing that
|
|
383
|
+
* ships.
|
|
384
|
+
*/
|
|
385
|
+
export declare const toDetectorClassName: (name: string) => string;
|
|
386
|
+
/** The producible key shape: `<concept>_present`, `<concept>_count`. */
|
|
387
|
+
export declare const CONCEPT_KEY_RE: RegExp;
|
|
388
|
+
/**
|
|
389
|
+
* How the service joins the names of the models that answered one frame.
|
|
390
|
+
*
|
|
391
|
+
* `MODEL_JOIN` in `deploy/detector/trillboards_detector/server.py`, where
|
|
392
|
+
* `model` is `MODEL_JOIN.join(batch.models)` — one name on a single-model box, N
|
|
393
|
+
* for a request the registry split across several. Held here because the
|
|
394
|
+
* attribution check below has to take that string apart again, and a wrong
|
|
395
|
+
* separator would silently make every merged answer look unattributed.
|
|
396
|
+
*/
|
|
397
|
+
export declare const DETECTOR_MODEL_JOIN = "+";
|
|
398
|
+
/** A producer field this detector can answer, once its name has been read. */
|
|
399
|
+
export interface MappedProducerField extends ProducerField {
|
|
400
|
+
/** The concept, in the field's own identifier spelling. */
|
|
401
|
+
concept: string;
|
|
402
|
+
/** The class to ask for, in the service's wire spelling. */
|
|
403
|
+
className: string;
|
|
404
|
+
kind: 'present' | 'count';
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Read a declared field name, or say why it cannot be read.
|
|
408
|
+
*
|
|
409
|
+
* The SUFFIX decides the computation — that is the vocabulary the corpus is
|
|
410
|
+
* written in and the only self-describing signal on the entry. The declared type
|
|
411
|
+
* is then checked AGAINST it rather than consulted instead of it: a null type is
|
|
412
|
+
* fine (plenty of registry rows carry none), and a type that contradicts the
|
|
413
|
+
* name is a defective row rather than an instruction.
|
|
414
|
+
*/
|
|
415
|
+
export declare function classifyProducerField(entry: ProducerField): MappedProducerField | {
|
|
416
|
+
reason: ProducerWithholdReason;
|
|
417
|
+
detail: string;
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* How many instances of each asked class this answer contains.
|
|
421
|
+
*
|
|
422
|
+
* Ported decision-for-decision from `detectorCorpusEndpoint.countsForConcepts`,
|
|
423
|
+
* because the corpus that gates a rollout and the producer that reads the model
|
|
424
|
+
* in the field must count the same way.
|
|
425
|
+
*
|
|
426
|
+
* @throws when the service returned a label nobody asked for — see below.
|
|
427
|
+
*/
|
|
428
|
+
export declare function countsForClasses(classes: string[], detections: Array<{
|
|
429
|
+
label: string;
|
|
430
|
+
confidence: number;
|
|
431
|
+
}>, classThresholds?: Map<string, number>): Map<string, number>;
|
|
432
|
+
export interface DetectorFieldProducerOptions {
|
|
433
|
+
/** Detector service base URL. Defaults to the client's loopback default. */
|
|
434
|
+
baseUrl?: string;
|
|
435
|
+
/** Per-call transport timeout. Default DEFAULT_PRODUCER_TIMEOUT_MS. */
|
|
436
|
+
timeoutMs?: number;
|
|
437
|
+
/** Total budget for one frame's calls. Default DEFAULT_PRODUCE_DEADLINE_MS. 0 disables. */
|
|
438
|
+
deadlineMs?: number;
|
|
439
|
+
/**
|
|
440
|
+
* Per-class operating points in WIRE spelling, as the models' own score cards
|
|
441
|
+
* fitted them — `modelClassLiveness.livenessFromScoreCard` writes them and
|
|
442
|
+
* `detectorCorpusEndpoint.thresholdsFromLivenessEvidence` folds them. A class
|
|
443
|
+
* with no entry is counted at `minConfidence`.
|
|
444
|
+
*/
|
|
445
|
+
classThresholds?: Map<string, number>;
|
|
446
|
+
/** Floor for a class whose model measured none. Default DEFAULT_PRODUCER_MIN_CONFIDENCE. */
|
|
447
|
+
minConfidence?: number;
|
|
448
|
+
/** Clock injection for tests. Default Date.now. */
|
|
449
|
+
now?: () => number;
|
|
450
|
+
}
|
|
451
|
+
export declare class DetectorFieldProducer {
|
|
452
|
+
private readonly baseUrl;
|
|
453
|
+
private readonly timeoutMs;
|
|
454
|
+
private readonly deadlineMs;
|
|
455
|
+
private readonly classThresholds;
|
|
456
|
+
private readonly minConfidence;
|
|
457
|
+
private readonly now;
|
|
458
|
+
private readonly clientFor;
|
|
459
|
+
constructor(options?: DetectorFieldProducerOptions, clientFactory?: DetectorClientFactory);
|
|
460
|
+
/**
|
|
461
|
+
* Produce this frame's declared fields.
|
|
462
|
+
*
|
|
463
|
+
* NEVER THROWS and never returns a promise that rejects: it would sit on the
|
|
464
|
+
* path to the only thing that produces alerts, and a producer that can fail a
|
|
465
|
+
* capture tick is worse than one that reports it has no reading.
|
|
466
|
+
*
|
|
467
|
+
* @param jpeg the frame's bytes
|
|
468
|
+
* @param fields the `producer` half of this profile's field partition
|
|
469
|
+
*/
|
|
470
|
+
produce(jpeg: Buffer, fields: ProducerField[]): Promise<ProducedFieldSet>;
|
|
471
|
+
/**
|
|
472
|
+
* Every field of one model, withheld together.
|
|
473
|
+
*
|
|
474
|
+
* A model that did not answer did not answer for ANY of its fields. Booking
|
|
475
|
+
* them one at a time would invite a partial group, which is the collapse this
|
|
476
|
+
* whole file is written against.
|
|
477
|
+
*/
|
|
478
|
+
private withholdGroup;
|
|
479
|
+
}
|
|
480
|
+
/** Flatten an answer to the `{field: value}` map a payload carries. */
|
|
481
|
+
export declare function toFieldValues(produced: ProducedField[]): Record<string, ProducedFieldValue>;
|
|
482
|
+
//# sourceMappingURL=DetectorFieldProducer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DetectorFieldProducer.d.ts","sourceRoot":"","sources":["../src/DetectorFieldProducer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6LG;AACH,OAAO,EAAE,mBAAmB,EAA4B,MAAM,uBAAuB,CAAC;AAEtF;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,4EAA4E;AAC5E,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,kBAAkB,CAAC;IAC1B,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,aAAa,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,sDAAsD;AACtD,eAAO,MAAM,yBAAyB;IACpC;;;;;;;;;OASG;;IAEH;;;;;;;;OAQG;;IAEH;;;;;;;;OAQG;;IAEH;;;;;;;;;;;;;;;OAeG;;IAEH;;;;;;OAMG;;CAEK,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAChC,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,OAAO,yBAAyB,CAAC,CAAC;AAE7E,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,QAAQ,EAAE,qBAAqB,EAAE,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;AAEtE,yEAAyE;AACzE,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE;IAC5C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,KAAK,mBAAmB,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,+BAA+B,OAAO,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,OAAQ,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,2BAA2B,OAAQ,CAAC;AAEjD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,MACO,CAAC;AAE3D,wEAAwE;AACxE,eAAO,MAAM,cAAc,QAA2B,CAAC;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAOvC,8EAA8E;AAC9E,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,aAAa,GACnB,mBAAmB,GAAG;IAAE,MAAM,EAAE,sBAAsB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA8B1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,EACxD,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAwDrB;AAED,MAAM,WAAW,4BAA4B;IAC3C,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsB;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;gBAEtC,OAAO,GAAE,4BAAiC,EAAE,aAAa,CAAC,EAAE,qBAAqB;IAmB7F;;;;;;;;;OASG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyL/E;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;CAgBtB;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAI3F"}
|