@camstack/addon-post-analysis 1.2.190 → 1.2.191
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/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CvZ5c9yG.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CHQmp5yH.mjs} +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BK6J3Mhb.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-B66vQbEn.mjs → hostInit-CAF5wBAZ.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +628 -431
- package/dist/pipeline-analytics/index.mjs +628 -431
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Ddpv6_VF.mjs +0 -26
|
@@ -33253,6 +33253,52 @@ var ClosedRowMediaGate = class {
|
|
|
33253
33253
|
}
|
|
33254
33254
|
};
|
|
33255
33255
|
//#endregion
|
|
33256
|
+
//#region src/pipeline-analytics/pipeline/co-moving-subjects.ts
|
|
33257
|
+
/**
|
|
33258
|
+
* Report at this much containment. A rider and a bicycle overlap heavily; two
|
|
33259
|
+
* people passing each other in the frame do not stay contained for long.
|
|
33260
|
+
* Deliberately a REPORTING threshold, not a decision one — nothing branches on
|
|
33261
|
+
* it, so it can be loose enough to see the near misses.
|
|
33262
|
+
*/
|
|
33263
|
+
var CO_MOVING_MIN_CONTAINMENT = .6;
|
|
33264
|
+
function intersectionArea$4(a, b) {
|
|
33265
|
+
return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
33266
|
+
}
|
|
33267
|
+
/**
|
|
33268
|
+
* The overlaps a NEWLY born track has with live tracks of a DIFFERENT class.
|
|
33269
|
+
*
|
|
33270
|
+
* Same-class overlaps are excluded: those are the tracker's own duplicate
|
|
33271
|
+
* problem and are already measured elsewhere. A zero-area box reports nothing
|
|
33272
|
+
* rather than dividing by zero.
|
|
33273
|
+
*/
|
|
33274
|
+
function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMENT) {
|
|
33275
|
+
const out = [];
|
|
33276
|
+
for (const b of born) {
|
|
33277
|
+
const areaB = b.bbox.w * b.bbox.h;
|
|
33278
|
+
if (areaB <= 0) continue;
|
|
33279
|
+
for (const o of live) {
|
|
33280
|
+
if (o.trackId === b.trackId) continue;
|
|
33281
|
+
if (o.className === b.className) continue;
|
|
33282
|
+
const areaO = o.bbox.w * o.bbox.h;
|
|
33283
|
+
if (areaO <= 0) continue;
|
|
33284
|
+
const inter = intersectionArea$4(b.bbox, o.bbox);
|
|
33285
|
+
if (inter <= 0) continue;
|
|
33286
|
+
const union = areaB + areaO - inter;
|
|
33287
|
+
const containment = inter / Math.min(areaB, areaO);
|
|
33288
|
+
if (containment < minContainment) continue;
|
|
33289
|
+
out.push({
|
|
33290
|
+
trackId: b.trackId,
|
|
33291
|
+
className: b.className,
|
|
33292
|
+
otherTrackId: o.trackId,
|
|
33293
|
+
otherClassName: o.className,
|
|
33294
|
+
iou: union > 0 ? inter / union : 0,
|
|
33295
|
+
containment
|
|
33296
|
+
});
|
|
33297
|
+
}
|
|
33298
|
+
}
|
|
33299
|
+
return out;
|
|
33300
|
+
}
|
|
33301
|
+
//#endregion
|
|
33256
33302
|
//#region src/pipeline-analytics/pipeline/edge-clear.ts
|
|
33257
33303
|
/**
|
|
33258
33304
|
* edge-clear — pure geometry for "is the subject fully in frame?" best-frame
|
|
@@ -33473,52 +33519,6 @@ function isEdgeAwareNewBest(current, candidate, hysteresis) {
|
|
|
33473
33519
|
return false;
|
|
33474
33520
|
}
|
|
33475
33521
|
//#endregion
|
|
33476
|
-
//#region src/pipeline-analytics/pipeline/co-moving-subjects.ts
|
|
33477
|
-
/**
|
|
33478
|
-
* Report at this much containment. A rider and a bicycle overlap heavily; two
|
|
33479
|
-
* people passing each other in the frame do not stay contained for long.
|
|
33480
|
-
* Deliberately a REPORTING threshold, not a decision one — nothing branches on
|
|
33481
|
-
* it, so it can be loose enough to see the near misses.
|
|
33482
|
-
*/
|
|
33483
|
-
var CO_MOVING_MIN_CONTAINMENT = .6;
|
|
33484
|
-
function intersectionArea$4(a, b) {
|
|
33485
|
-
return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
33486
|
-
}
|
|
33487
|
-
/**
|
|
33488
|
-
* The overlaps a NEWLY born track has with live tracks of a DIFFERENT class.
|
|
33489
|
-
*
|
|
33490
|
-
* Same-class overlaps are excluded: those are the tracker's own duplicate
|
|
33491
|
-
* problem and are already measured elsewhere. A zero-area box reports nothing
|
|
33492
|
-
* rather than dividing by zero.
|
|
33493
|
-
*/
|
|
33494
|
-
function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMENT) {
|
|
33495
|
-
const out = [];
|
|
33496
|
-
for (const b of born) {
|
|
33497
|
-
const areaB = b.bbox.w * b.bbox.h;
|
|
33498
|
-
if (areaB <= 0) continue;
|
|
33499
|
-
for (const o of live) {
|
|
33500
|
-
if (o.trackId === b.trackId) continue;
|
|
33501
|
-
if (o.className === b.className) continue;
|
|
33502
|
-
const areaO = o.bbox.w * o.bbox.h;
|
|
33503
|
-
if (areaO <= 0) continue;
|
|
33504
|
-
const inter = intersectionArea$4(b.bbox, o.bbox);
|
|
33505
|
-
if (inter <= 0) continue;
|
|
33506
|
-
const union = areaB + areaO - inter;
|
|
33507
|
-
const containment = inter / Math.min(areaB, areaO);
|
|
33508
|
-
if (containment < minContainment) continue;
|
|
33509
|
-
out.push({
|
|
33510
|
-
trackId: b.trackId,
|
|
33511
|
-
className: b.className,
|
|
33512
|
-
otherTrackId: o.trackId,
|
|
33513
|
-
otherClassName: o.className,
|
|
33514
|
-
iou: union > 0 ? inter / union : 0,
|
|
33515
|
-
containment
|
|
33516
|
-
});
|
|
33517
|
-
}
|
|
33518
|
-
}
|
|
33519
|
-
return out;
|
|
33520
|
-
}
|
|
33521
|
-
//#endregion
|
|
33522
33522
|
//#region src/pipeline-analytics/pipeline/first-frame-retry-bbox.ts
|
|
33523
33523
|
/** Crop box for a late firstFrame retry: the BIRTH geometry, not the
|
|
33524
33524
|
* later frozen/coasted box (track 1b659d56). */
|
|
@@ -34042,7 +34042,7 @@ var MAX_PATH_LENGTH = 300;
|
|
|
34042
34042
|
function clamp(value, min, max) {
|
|
34043
34043
|
return Math.max(min, Math.min(max, value));
|
|
34044
34044
|
}
|
|
34045
|
-
function iou$
|
|
34045
|
+
function iou$3(a, b) {
|
|
34046
34046
|
const ax1 = a.x, ay1 = a.y, ax2 = a.x + a.w, ay2 = a.y + a.h;
|
|
34047
34047
|
const bx1 = b.x, by1 = b.y, bx2 = b.x + b.w, by2 = b.y + b.h;
|
|
34048
34048
|
const ix1 = Math.max(ax1, bx1), iy1 = Math.max(ay1, by1);
|
|
@@ -34110,7 +34110,7 @@ var SortTracker = class SortTracker {
|
|
|
34110
34110
|
trackClass: track.class,
|
|
34111
34111
|
detClass: det.class,
|
|
34112
34112
|
detScore: det.score,
|
|
34113
|
-
iou: iou$
|
|
34113
|
+
iou: iou$3(track.bbox, det.bbox),
|
|
34114
34114
|
centroidDistPx: Math.hypot(tc.x - dc.x, tc.y - dc.y),
|
|
34115
34115
|
trackDiagPx: Math.hypot(track.bbox.w, track.bbox.h),
|
|
34116
34116
|
areaRatio: smaller > 0 ? Math.max(trackArea, detArea) / smaller : 0,
|
|
@@ -34174,14 +34174,14 @@ var SortTracker = class SortTracker {
|
|
|
34174
34174
|
isDuplicateSpawn(det, live, timestamp) {
|
|
34175
34175
|
for (const t of live) {
|
|
34176
34176
|
if (!this.dedupCompatible(t.class, det.class)) continue;
|
|
34177
|
-
if (iou$
|
|
34178
|
-
if (growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
|
|
34177
|
+
if (iou$3(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
|
|
34178
|
+
if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
|
|
34179
34179
|
this.reportBind("spawn-dedup", "growth", t, det, timestamp);
|
|
34180
34180
|
return true;
|
|
34181
34181
|
}
|
|
34182
34182
|
}
|
|
34183
34183
|
if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.score < this.config.animalOverPersonMaxScore) {
|
|
34184
|
-
for (const t of live) if (t.class === PERSON_CLASS && iou$
|
|
34184
|
+
for (const t of live) if (t.class === PERSON_CLASS && iou$3(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
|
|
34185
34185
|
}
|
|
34186
34186
|
return false;
|
|
34187
34187
|
}
|
|
@@ -34221,7 +34221,7 @@ var SortTracker = class SortTracker {
|
|
|
34221
34221
|
if (dropIds.has(a.id) || dropIds.has(b.id)) continue;
|
|
34222
34222
|
if (!this.dedupCompatible(a.class, b.class)) continue;
|
|
34223
34223
|
const key = SortTracker.pairKey(a.id, b.id);
|
|
34224
|
-
if (iou$
|
|
34224
|
+
if (iou$3(a.bbox, b.bbox) < this.config.dedupMergeIou) {
|
|
34225
34225
|
this.dupStreaks.delete(key);
|
|
34226
34226
|
continue;
|
|
34227
34227
|
}
|
|
@@ -34297,7 +34297,7 @@ var SortTracker = class SortTracker {
|
|
|
34297
34297
|
*/
|
|
34298
34298
|
looseMatch(track, det, timestamp) {
|
|
34299
34299
|
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
|
|
34300
|
-
if (iou$
|
|
34300
|
+
if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
|
|
34301
34301
|
const tc = bboxCentroid(track.bbox);
|
|
34302
34302
|
const dc = bboxCentroid(det.bbox);
|
|
34303
34303
|
const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
|
|
@@ -34383,7 +34383,7 @@ var SortTracker = class SortTracker {
|
|
|
34383
34383
|
if (used.has(di)) continue;
|
|
34384
34384
|
const det = detections[di];
|
|
34385
34385
|
if (!this.associable(track.class, det.class)) continue;
|
|
34386
|
-
const score = iou$
|
|
34386
|
+
const score = iou$3(pbox, det.bbox);
|
|
34387
34387
|
if (score >= iouThreshold) pairs.push({
|
|
34388
34388
|
track,
|
|
34389
34389
|
detIdx: di,
|
|
@@ -34445,7 +34445,7 @@ var SortTracker = class SortTracker {
|
|
|
34445
34445
|
rescuePairs.push({
|
|
34446
34446
|
track,
|
|
34447
34447
|
detIdx: di,
|
|
34448
|
-
score: iou$
|
|
34448
|
+
score: iou$3(track.bbox, det.bbox),
|
|
34449
34449
|
gate
|
|
34450
34450
|
});
|
|
34451
34451
|
}
|
|
@@ -34522,7 +34522,7 @@ var SortTracker = class SortTracker {
|
|
|
34522
34522
|
const gate = this.looseMatch(lost, det, timestamp);
|
|
34523
34523
|
if (gate === null) continue;
|
|
34524
34524
|
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
|
|
34525
|
-
const score = iou$
|
|
34525
|
+
const score = iou$3(lost.bbox, det.bbox);
|
|
34526
34526
|
const peers = this.sameGroupPeerCount(lost, surviving);
|
|
34527
34527
|
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
34528
34528
|
if (score > refusedScore) {
|
|
@@ -44300,6 +44300,32 @@ var AnalyticsQueryFacade = class {
|
|
|
44300
44300
|
}
|
|
44301
44301
|
};
|
|
44302
44302
|
//#endregion
|
|
44303
|
+
//#region src/pipeline-analytics/audio-confirm-window.ts
|
|
44304
|
+
function emptyAudioConfirmState() {
|
|
44305
|
+
return { hitsByClass: /* @__PURE__ */ new Map() };
|
|
44306
|
+
}
|
|
44307
|
+
function observeAudioConfirm(state, input, cfg) {
|
|
44308
|
+
if (cfg.confirmHits <= 1) return {
|
|
44309
|
+
confirmed: true,
|
|
44310
|
+
nextState: state
|
|
44311
|
+
};
|
|
44312
|
+
const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
|
|
44313
|
+
const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
|
|
44314
|
+
const next = new Map(state.hitsByClass);
|
|
44315
|
+
if (kept.length < cfg.confirmHits) {
|
|
44316
|
+
next.set(input.className, kept);
|
|
44317
|
+
return {
|
|
44318
|
+
confirmed: false,
|
|
44319
|
+
nextState: { hitsByClass: next }
|
|
44320
|
+
};
|
|
44321
|
+
}
|
|
44322
|
+
next.delete(input.className);
|
|
44323
|
+
return {
|
|
44324
|
+
confirmed: true,
|
|
44325
|
+
nextState: { hitsByClass: next }
|
|
44326
|
+
};
|
|
44327
|
+
}
|
|
44328
|
+
//#endregion
|
|
44303
44329
|
//#region src/pipeline-analytics/audio-detection-settings.ts
|
|
44304
44330
|
var AudioDetectionSettingsSchema = object({
|
|
44305
44331
|
classificationMinScore: number().min(0).max(1).default(.6),
|
|
@@ -44560,32 +44586,6 @@ function observeAudioEpisode(state, sample, knobs) {
|
|
|
44560
44586
|
};
|
|
44561
44587
|
}
|
|
44562
44588
|
//#endregion
|
|
44563
|
-
//#region src/pipeline-analytics/audio-confirm-window.ts
|
|
44564
|
-
function emptyAudioConfirmState() {
|
|
44565
|
-
return { hitsByClass: /* @__PURE__ */ new Map() };
|
|
44566
|
-
}
|
|
44567
|
-
function observeAudioConfirm(state, input, cfg) {
|
|
44568
|
-
if (cfg.confirmHits <= 1) return {
|
|
44569
|
-
confirmed: true,
|
|
44570
|
-
nextState: state
|
|
44571
|
-
};
|
|
44572
|
-
const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
|
|
44573
|
-
const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
|
|
44574
|
-
const next = new Map(state.hitsByClass);
|
|
44575
|
-
if (kept.length < cfg.confirmHits) {
|
|
44576
|
-
next.set(input.className, kept);
|
|
44577
|
-
return {
|
|
44578
|
-
confirmed: false,
|
|
44579
|
-
nextState: { hitsByClass: next }
|
|
44580
|
-
};
|
|
44581
|
-
}
|
|
44582
|
-
next.delete(input.className);
|
|
44583
|
-
return {
|
|
44584
|
-
confirmed: true,
|
|
44585
|
-
nextState: { hitsByClass: next }
|
|
44586
|
-
};
|
|
44587
|
-
}
|
|
44588
|
-
//#endregion
|
|
44589
44589
|
//#region src/pipeline-analytics/audio-event-router.ts
|
|
44590
44590
|
/**
|
|
44591
44591
|
* Decide whether an audio frame is a (meaningful) classification event or a
|
|
@@ -45166,6 +45166,7 @@ async function runConfirmation(candidate, config, deps, exhausted) {
|
|
|
45166
45166
|
} else if (!bestIncompatible || d.score > bestIncompatible.score) bestIncompatible = d;
|
|
45167
45167
|
const bar = Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
|
|
45168
45168
|
const confirmed = best !== void 0 && best.score >= bar;
|
|
45169
|
+
const secondary = bestIncompatible ? promoteSecondary(candidate, bestIncompatible, bar, deps) : null;
|
|
45169
45170
|
return {
|
|
45170
45171
|
trackId: candidate.trackId,
|
|
45171
45172
|
verdict: confirmed ? "confirmed" : "suppressed",
|
|
@@ -45176,7 +45177,30 @@ async function runConfirmation(candidate, config, deps, exhausted) {
|
|
|
45176
45177
|
...bestIncompatible ? {
|
|
45177
45178
|
bestIncompatibleClass: bestIncompatible.macroClass,
|
|
45178
45179
|
bestIncompatibleScore: bestIncompatible.score
|
|
45179
|
-
} : {}
|
|
45180
|
+
} : {},
|
|
45181
|
+
...secondary ? { secondary } : {}
|
|
45182
|
+
};
|
|
45183
|
+
}
|
|
45184
|
+
/**
|
|
45185
|
+
* The incompatible best becomes a secondary subject when it clears the SAME
|
|
45186
|
+
* bar the candidate faced and can be placed in the frame. The crop-normalised
|
|
45187
|
+
* box is scaled into the crop's frame rectangle — the one derivation the
|
|
45188
|
+
* capture used, supplied by the caller, never re-derived here.
|
|
45189
|
+
*/
|
|
45190
|
+
function promoteSecondary(candidate, detection, bar, deps) {
|
|
45191
|
+
if (detection.score < bar || detection.bbox === void 0) return null;
|
|
45192
|
+
const rect = deps.cropRectFor?.(candidate) ?? null;
|
|
45193
|
+
if (rect === null || rect.w <= 0 || rect.h <= 0) return null;
|
|
45194
|
+
const box = detection.bbox;
|
|
45195
|
+
return {
|
|
45196
|
+
className: detection.macroClass,
|
|
45197
|
+
score: detection.score,
|
|
45198
|
+
bbox: {
|
|
45199
|
+
x: rect.x + box.x * rect.w,
|
|
45200
|
+
y: rect.y + box.y * rect.h,
|
|
45201
|
+
w: box.w * rect.w,
|
|
45202
|
+
h: box.h * rect.h
|
|
45203
|
+
}
|
|
45180
45204
|
};
|
|
45181
45205
|
}
|
|
45182
45206
|
async function confirmOne(candidate, config, deps, exhausted) {
|
|
@@ -47936,6 +47960,336 @@ var MediaWriteGate = class {
|
|
|
47936
47960
|
this.paused = false;
|
|
47937
47961
|
}
|
|
47938
47962
|
};
|
|
47963
|
+
//#endregion
|
|
47964
|
+
//#region src/pipeline-analytics/runtime/failure-report.ts
|
|
47965
|
+
/**
|
|
47966
|
+
* What `pipeline-analytics` reports through `failure-contribution` — the four
|
|
47967
|
+
* per-camera counters, and the ONE place their vocabulary is written down.
|
|
47968
|
+
*
|
|
47969
|
+
* ## Why the vocabulary is a module and not four string literals
|
|
47970
|
+
*
|
|
47971
|
+
* D276 gave a track row a `previewMissReason` composed from strings like
|
|
47972
|
+
* `keyframe:native-miss` and `thumbnail:overflow`. Those strings are now
|
|
47973
|
+
* joinable evidence: a row says which track lost its tile, and these counters
|
|
47974
|
+
* say how often that camera loses one and out of how many tries. A second
|
|
47975
|
+
* vocabulary for the same loss would break the join silently — the row would
|
|
47976
|
+
* say `native-miss` and the counter `nativeMiss`, and nobody would notice until
|
|
47977
|
+
* somebody tried to correlate them. So the reasons live here, named once, and
|
|
47978
|
+
* they deliberately reuse D276's spelling.
|
|
47979
|
+
*
|
|
47980
|
+
* ## Every family carries its denominator
|
|
47981
|
+
*
|
|
47982
|
+
* Each `note*` call is ONE attempt. The success calls are not decoration: a
|
|
47983
|
+
* failure count without them is the number that read as "35x worse than
|
|
47984
|
+
* yesterday" on 2026-08-28 and was flat all day once divided
|
|
47985
|
+
* (`packages/types/src/metrics/failure-counters.ts`).
|
|
47986
|
+
*
|
|
47987
|
+
* ## Nothing here logs
|
|
47988
|
+
*
|
|
47989
|
+
* These paths run at frame rate — 507 enrichment-crop misses in five minutes at
|
|
47990
|
+
* peak, ~100/min. A line each is its own defect, and the log lines that already
|
|
47991
|
+
* exist on these paths (`enrichment crop native miss`, `capture queue full`,
|
|
47992
|
+
* `plate: read discarded`) are unchanged and still throttled. This is the
|
|
47993
|
+
* counter that makes them countable without reading them.
|
|
47994
|
+
*/
|
|
47995
|
+
/**
|
|
47996
|
+
* The enrichment crop capture — face / plate / clip / detail-classifier.
|
|
47997
|
+
* Denominator: every crop this addon asked the owning runner for.
|
|
47998
|
+
*/
|
|
47999
|
+
var FAMILY_ENRICHMENT_CROP = "enrichment-crop";
|
|
48000
|
+
/**
|
|
48001
|
+
* The per-camera capture scheduler lane. Denominator: every queued capture that
|
|
48002
|
+
* reached a TERMINAL scheduling outcome — started, or dropped. A coalesced or
|
|
48003
|
+
* cancelled request is neither and is deliberately not counted: a track that
|
|
48004
|
+
* closed before its capture ran did not lose work, it stopped wanting it.
|
|
48005
|
+
*/
|
|
48006
|
+
var FAMILY_CAPTURE_SCHEDULE = "capture-schedule";
|
|
48007
|
+
/** Plate OCR reads routed from the detail plane. Denominator: every read. */
|
|
48008
|
+
var FAMILY_PLATE_OCR = "plate-ocr";
|
|
48009
|
+
/**
|
|
48010
|
+
* Person tracks born on this camera. Denominator: every confirmed person birth.
|
|
48011
|
+
*
|
|
48012
|
+
* The "failure" here is NOT suppressed and must not be — a person standing over
|
|
48013
|
+
* a car is a real person, and this repo will not trade that away for a cleaner
|
|
48014
|
+
* count (D275 made the same choice for stationary suppression). It is counted so
|
|
48015
|
+
* the phantom rate is a number instead of an impression: 4 of 23 on camera 617
|
|
48016
|
+
* in two hours, measured by hand on 2026-08-28.
|
|
48017
|
+
*/
|
|
48018
|
+
var FAMILY_PERSON_BIRTH = "person-birth";
|
|
48019
|
+
/**
|
|
48020
|
+
* The owning runner had no native pixels for the ROI.
|
|
48021
|
+
*
|
|
48022
|
+
* This collapses the runner-side causes the operator can already see in logs
|
|
48023
|
+
* (`worker-lease-gone`, `no-registry-entry`, `jpeg-encode-failed`) because
|
|
48024
|
+
* `pipelineRunner.getNativeCrop` answers an ROI miss with `null` and carries no
|
|
48025
|
+
* reason on the wire — and `FrameHandle` has no `deviceId`, so the runner could
|
|
48026
|
+
* not attribute the split per camera even if it reported it. Splitting this is a
|
|
48027
|
+
* framework change (a reason on the reply); until then the honest name is the
|
|
48028
|
+
* one the caller can actually stand behind.
|
|
48029
|
+
*/
|
|
48030
|
+
var REASON_NATIVE_MISS = "native-miss";
|
|
48031
|
+
/** Queued past `staleAfterMs`; the frame it wanted is gone. */
|
|
48032
|
+
var REASON_STALE_DROP = "stale-drop";
|
|
48033
|
+
/** Shed because the camera's lane was at its queue cap. */
|
|
48034
|
+
var REASON_QUEUE_OVERFLOW = "queue-overflow";
|
|
48035
|
+
/**
|
|
48036
|
+
* The OCR step returned, with nothing in it.
|
|
48037
|
+
*
|
|
48038
|
+
* The defect this counter was built for: ~35% of plate reads came back with an
|
|
48039
|
+
* empty label and fell through the detail router's `d.label.length > 0` gate
|
|
48040
|
+
* into no branch at all — no store, no log, no row. Silence read as "this camera
|
|
48041
|
+
* never saw a plate", which is a completely different fix.
|
|
48042
|
+
*/
|
|
48043
|
+
var REASON_EMPTY_READ = "empty-read";
|
|
48044
|
+
/** A plate detail with no bbox — nothing to crop, nothing to place. */
|
|
48045
|
+
var REASON_NO_BBOX = "no-bbox";
|
|
48046
|
+
/** `isPlausiblePlateRead` refused it (junk OCR off a distant/oblique plate). */
|
|
48047
|
+
var REASON_IMPLAUSIBLE = "implausible";
|
|
48048
|
+
/** Below the camera's configured minimum OCR confidence. */
|
|
48049
|
+
var REASON_BELOW_MIN_CONFIDENCE = "below-min-confidence";
|
|
48050
|
+
/** A person born with its box inside a vehicle's. See {@link FAMILY_PERSON_BIRTH}. */
|
|
48051
|
+
var REASON_PERSON_OVER_VEHICLE = "person-over-vehicle";
|
|
48052
|
+
/**
|
|
48053
|
+
* This addon's `failure-contribution` counters.
|
|
48054
|
+
*
|
|
48055
|
+
* One instance per addon process, handed to the paths that fail. Every method
|
|
48056
|
+
* takes the numeric `deviceId` first, for the same reason every log line does:
|
|
48057
|
+
* the question is always per camera.
|
|
48058
|
+
*/
|
|
48059
|
+
var AnalyticsFailureReport = class {
|
|
48060
|
+
now;
|
|
48061
|
+
counters;
|
|
48062
|
+
constructor(now = Date.now, counters = new FailureCounters()) {
|
|
48063
|
+
this.now = now;
|
|
48064
|
+
this.counters = counters;
|
|
48065
|
+
}
|
|
48066
|
+
/** One enrichment crop capture. `reason` absent = the native ROI landed. */
|
|
48067
|
+
noteEnrichmentCrop(deviceId, reason) {
|
|
48068
|
+
this.counters.note({
|
|
48069
|
+
deviceId,
|
|
48070
|
+
family: FAMILY_ENRICHMENT_CROP,
|
|
48071
|
+
...reason !== void 0 ? { reason } : {}
|
|
48072
|
+
}, this.now());
|
|
48073
|
+
}
|
|
48074
|
+
/** One capture that reached a terminal scheduling outcome. */
|
|
48075
|
+
noteCaptureSchedule(deviceId, reason) {
|
|
48076
|
+
this.counters.note({
|
|
48077
|
+
deviceId,
|
|
48078
|
+
family: FAMILY_CAPTURE_SCHEDULE,
|
|
48079
|
+
...reason !== void 0 ? { reason } : {}
|
|
48080
|
+
}, this.now());
|
|
48081
|
+
}
|
|
48082
|
+
/** One plate OCR read. `reason` absent = the read was accepted and stored. */
|
|
48083
|
+
notePlateRead(deviceId, reason) {
|
|
48084
|
+
this.counters.note({
|
|
48085
|
+
deviceId,
|
|
48086
|
+
family: FAMILY_PLATE_OCR,
|
|
48087
|
+
...reason !== void 0 ? { reason } : {}
|
|
48088
|
+
}, this.now());
|
|
48089
|
+
}
|
|
48090
|
+
/** One confirmed person birth. `reason` present = it was born over a vehicle. */
|
|
48091
|
+
notePersonBirth(deviceId, reason) {
|
|
48092
|
+
this.counters.note({
|
|
48093
|
+
deviceId,
|
|
48094
|
+
family: FAMILY_PERSON_BIRTH,
|
|
48095
|
+
...reason !== void 0 ? { reason } : {}
|
|
48096
|
+
}, this.now());
|
|
48097
|
+
}
|
|
48098
|
+
/** The `failure-contribution` provider's payload. Reads, never resets. */
|
|
48099
|
+
list() {
|
|
48100
|
+
return this.counters.snapshot(this.now());
|
|
48101
|
+
}
|
|
48102
|
+
/** Addon disposal. */
|
|
48103
|
+
clear() {
|
|
48104
|
+
this.counters.clear();
|
|
48105
|
+
}
|
|
48106
|
+
};
|
|
48107
|
+
//#endregion
|
|
48108
|
+
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
48109
|
+
/**
|
|
48110
|
+
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
48111
|
+
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
48112
|
+
*
|
|
48113
|
+
* Formula (before clamping):
|
|
48114
|
+
* newX = x - padding * w
|
|
48115
|
+
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
48116
|
+
* newY = y - padding * h
|
|
48117
|
+
* newH = h + 2 * padding * h
|
|
48118
|
+
*
|
|
48119
|
+
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
48120
|
+
* new object.
|
|
48121
|
+
*
|
|
48122
|
+
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
48123
|
+
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
48124
|
+
* It shrinks only when the padded window is larger than the frame itself
|
|
48125
|
+
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
48126
|
+
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
48127
|
+
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
48128
|
+
*/
|
|
48129
|
+
function padBbox(bbox, padding) {
|
|
48130
|
+
const rawX = bbox.x - padding * bbox.w;
|
|
48131
|
+
const rawY = bbox.y - padding * bbox.h;
|
|
48132
|
+
const rawW = bbox.w * (1 + 2 * padding);
|
|
48133
|
+
const rawH = bbox.h * (1 + 2 * padding);
|
|
48134
|
+
const w = Math.min(rawW, 1);
|
|
48135
|
+
const h = Math.min(rawH, 1);
|
|
48136
|
+
return {
|
|
48137
|
+
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
48138
|
+
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
48139
|
+
w,
|
|
48140
|
+
h
|
|
48141
|
+
};
|
|
48142
|
+
}
|
|
48143
|
+
//#endregion
|
|
48144
|
+
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
48145
|
+
/**
|
|
48146
|
+
* The normalised window a capture asks the runner for: the box padded by
|
|
48147
|
+
* `padding` and slid inside the unit square. ONE derivation, shared by the
|
|
48148
|
+
* capture below and by any caller that must put a crop-relative box back
|
|
48149
|
+
* into the frame (the confirmation gate's secondary subject) — two copies of
|
|
48150
|
+
* this arithmetic would drift, and a drifted one places the subject somewhere
|
|
48151
|
+
* it is not.
|
|
48152
|
+
*/
|
|
48153
|
+
function captureCropWindow(bbox, frameWidth, frameHeight, padding) {
|
|
48154
|
+
return padBbox({
|
|
48155
|
+
x: bbox.x / frameWidth,
|
|
48156
|
+
y: bbox.y / frameHeight,
|
|
48157
|
+
w: bbox.w / frameWidth,
|
|
48158
|
+
h: bbox.h / frameHeight
|
|
48159
|
+
}, padding);
|
|
48160
|
+
}
|
|
48161
|
+
/** {@link captureCropWindow} in FRAME pixels. */
|
|
48162
|
+
function captureCropFrameRect(bbox, frameWidth, frameHeight, padding) {
|
|
48163
|
+
const window = captureCropWindow(bbox, frameWidth, frameHeight, padding);
|
|
48164
|
+
return {
|
|
48165
|
+
x: window.x * frameWidth,
|
|
48166
|
+
y: window.y * frameHeight,
|
|
48167
|
+
w: window.w * frameWidth,
|
|
48168
|
+
h: window.h * frameHeight
|
|
48169
|
+
};
|
|
48170
|
+
}
|
|
48171
|
+
function createCaptureCrop(deps) {
|
|
48172
|
+
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
48173
|
+
const paddedNorm = captureCropWindow(bbox, frameWidth, frameHeight, padding);
|
|
48174
|
+
const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
48175
|
+
if (nativeCrop) {
|
|
48176
|
+
deps.bumpCropMetric(true);
|
|
48177
|
+
if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId);
|
|
48178
|
+
return nativeCrop;
|
|
48179
|
+
}
|
|
48180
|
+
deps.bumpCropMetric(false);
|
|
48181
|
+
if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId, REASON_NATIVE_MISS);
|
|
48182
|
+
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
|
|
48183
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
48184
|
+
meta: { nodeId: frameHandle.nodeId }
|
|
48185
|
+
});
|
|
48186
|
+
return null;
|
|
48187
|
+
};
|
|
48188
|
+
}
|
|
48189
|
+
//#endregion
|
|
48190
|
+
//#region src/pipeline-analytics/pipeline/secondary-subjects.ts
|
|
48191
|
+
/**
|
|
48192
|
+
* How long a promoted subject may keep riding. Rides are consumed within
|
|
48193
|
+
* ~300 ms at 10 fps; this bound is what stops a promotion made just before a
|
|
48194
|
+
* stall from riding a frame that belongs to a scene that has moved on.
|
|
48195
|
+
*/
|
|
48196
|
+
var SECONDARY_SUBJECT_TTL_MS = 3e3;
|
|
48197
|
+
var SecondarySubjectQueue = class {
|
|
48198
|
+
byKey = /* @__PURE__ */ new Map();
|
|
48199
|
+
nextId = 0;
|
|
48200
|
+
add(key, subject, foundBesideTrackId, now) {
|
|
48201
|
+
const pending = this.byKey.get(key) ?? [];
|
|
48202
|
+
this.nextId += 1;
|
|
48203
|
+
this.byKey.set(key, [...pending, {
|
|
48204
|
+
id: `s${this.nextId}`,
|
|
48205
|
+
subject,
|
|
48206
|
+
foundBesideTrackId,
|
|
48207
|
+
expiresAt: now + SECONDARY_SUBJECT_TTL_MS,
|
|
48208
|
+
ridesLeft: 3
|
|
48209
|
+
}]);
|
|
48210
|
+
}
|
|
48211
|
+
/**
|
|
48212
|
+
* Every subject still owed a ride for `key`, one ride spent. An entry out of
|
|
48213
|
+
* rides or past its TTL is forgotten here rather than served.
|
|
48214
|
+
*/
|
|
48215
|
+
take(key, now) {
|
|
48216
|
+
const pending = this.byKey.get(key);
|
|
48217
|
+
if (!pending) return [];
|
|
48218
|
+
const live = pending.filter((entry) => entry.expiresAt > now && entry.ridesLeft > 0);
|
|
48219
|
+
const remaining = live.map((entry) => ({
|
|
48220
|
+
...entry,
|
|
48221
|
+
ridesLeft: entry.ridesLeft - 1
|
|
48222
|
+
})).filter((entry) => entry.ridesLeft > 0);
|
|
48223
|
+
if (remaining.length === 0) this.byKey.delete(key);
|
|
48224
|
+
else this.byKey.set(key, remaining);
|
|
48225
|
+
return live.map((entry) => ({
|
|
48226
|
+
id: entry.id,
|
|
48227
|
+
subject: entry.subject,
|
|
48228
|
+
foundBesideTrackId: entry.foundBesideTrackId
|
|
48229
|
+
}));
|
|
48230
|
+
}
|
|
48231
|
+
/** A subject a real detection has taken over rides no further. */
|
|
48232
|
+
drop(key, id) {
|
|
48233
|
+
const pending = this.byKey.get(key);
|
|
48234
|
+
if (!pending) return;
|
|
48235
|
+
const remaining = pending.filter((entry) => entry.id !== id);
|
|
48236
|
+
if (remaining.length === 0) this.byKey.delete(key);
|
|
48237
|
+
else this.byKey.set(key, remaining);
|
|
48238
|
+
}
|
|
48239
|
+
forget(key) {
|
|
48240
|
+
this.byKey.delete(key);
|
|
48241
|
+
}
|
|
48242
|
+
};
|
|
48243
|
+
function iou$2(a, b) {
|
|
48244
|
+
const inter = Math.max(0, Math.min(a.x + a.w, b.x + b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.height) - Math.max(a.y, b.y));
|
|
48245
|
+
const union = a.w * a.h + b.width * b.height - inter;
|
|
48246
|
+
return union > 0 ? inter / union : 0;
|
|
48247
|
+
}
|
|
48248
|
+
/**
|
|
48249
|
+
* The runner's detections plus one synthetic first-level detection per pending
|
|
48250
|
+
* subject that no same-class detection already covers. The synthetic box
|
|
48251
|
+
* carries the gate's score — a measurement on a native crop, not an invention
|
|
48252
|
+
* — and an id that says where it came from. The original result is not
|
|
48253
|
+
* mutated.
|
|
48254
|
+
*/
|
|
48255
|
+
function injectSecondarySubjects(frame, pending) {
|
|
48256
|
+
if (pending.length === 0) return {
|
|
48257
|
+
frame,
|
|
48258
|
+
covered: []
|
|
48259
|
+
};
|
|
48260
|
+
const added = [];
|
|
48261
|
+
const covered = [];
|
|
48262
|
+
for (const { id, subject, foundBesideTrackId } of pending) {
|
|
48263
|
+
if ([...frame.detections, ...added].some((d) => d.kind === "first-level" && d.macroClass === subject.className && iou$2(subject.bbox, d.bbox) >= .5)) {
|
|
48264
|
+
covered.push(id);
|
|
48265
|
+
continue;
|
|
48266
|
+
}
|
|
48267
|
+
added.push({
|
|
48268
|
+
id: `secondary-${id}-${foundBesideTrackId}`,
|
|
48269
|
+
kind: "first-level",
|
|
48270
|
+
macroClass: subject.className,
|
|
48271
|
+
score: subject.score,
|
|
48272
|
+
labels: [],
|
|
48273
|
+
bbox: {
|
|
48274
|
+
x: subject.bbox.x,
|
|
48275
|
+
y: subject.bbox.y,
|
|
48276
|
+
width: subject.bbox.w,
|
|
48277
|
+
height: subject.bbox.h
|
|
48278
|
+
}
|
|
48279
|
+
});
|
|
48280
|
+
}
|
|
48281
|
+
if (added.length === 0) return {
|
|
48282
|
+
frame,
|
|
48283
|
+
covered
|
|
48284
|
+
};
|
|
48285
|
+
return {
|
|
48286
|
+
frame: {
|
|
48287
|
+
...frame,
|
|
48288
|
+
detections: [...frame.detections, ...added]
|
|
48289
|
+
},
|
|
48290
|
+
covered
|
|
48291
|
+
};
|
|
48292
|
+
}
|
|
47939
48293
|
var MACRO_SET = new Set([
|
|
47940
48294
|
"person",
|
|
47941
48295
|
"vehicle",
|
|
@@ -51785,42 +52139,6 @@ function decideKeyFrameCapture(input) {
|
|
|
51785
52139
|
return { kind: "retry" };
|
|
51786
52140
|
}
|
|
51787
52141
|
//#endregion
|
|
51788
|
-
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
51789
|
-
/**
|
|
51790
|
-
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
51791
|
-
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
51792
|
-
*
|
|
51793
|
-
* Formula (before clamping):
|
|
51794
|
-
* newX = x - padding * w
|
|
51795
|
-
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
51796
|
-
* newY = y - padding * h
|
|
51797
|
-
* newH = h + 2 * padding * h
|
|
51798
|
-
*
|
|
51799
|
-
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
51800
|
-
* new object.
|
|
51801
|
-
*
|
|
51802
|
-
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
51803
|
-
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
51804
|
-
* It shrinks only when the padded window is larger than the frame itself
|
|
51805
|
-
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
51806
|
-
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
51807
|
-
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
51808
|
-
*/
|
|
51809
|
-
function padBbox(bbox, padding) {
|
|
51810
|
-
const rawX = bbox.x - padding * bbox.w;
|
|
51811
|
-
const rawY = bbox.y - padding * bbox.h;
|
|
51812
|
-
const rawW = bbox.w * (1 + 2 * padding);
|
|
51813
|
-
const rawH = bbox.h * (1 + 2 * padding);
|
|
51814
|
-
const w = Math.min(rawW, 1);
|
|
51815
|
-
const h = Math.min(rawH, 1);
|
|
51816
|
-
return {
|
|
51817
|
-
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
51818
|
-
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
51819
|
-
w,
|
|
51820
|
-
h
|
|
51821
|
-
};
|
|
51822
|
-
}
|
|
51823
|
-
//#endregion
|
|
51824
52142
|
//#region src/pipeline-analytics/pipeline/subject-agreement.ts
|
|
51825
52143
|
/**
|
|
51826
52144
|
* `subjectAgreement` — the ONE predicate for "a rectangle measured on frame A
|
|
@@ -55175,150 +55493,6 @@ function votePlateText(reads) {
|
|
|
55175
55493
|
return out;
|
|
55176
55494
|
}
|
|
55177
55495
|
//#endregion
|
|
55178
|
-
//#region src/pipeline-analytics/runtime/failure-report.ts
|
|
55179
|
-
/**
|
|
55180
|
-
* What `pipeline-analytics` reports through `failure-contribution` — the four
|
|
55181
|
-
* per-camera counters, and the ONE place their vocabulary is written down.
|
|
55182
|
-
*
|
|
55183
|
-
* ## Why the vocabulary is a module and not four string literals
|
|
55184
|
-
*
|
|
55185
|
-
* D276 gave a track row a `previewMissReason` composed from strings like
|
|
55186
|
-
* `keyframe:native-miss` and `thumbnail:overflow`. Those strings are now
|
|
55187
|
-
* joinable evidence: a row says which track lost its tile, and these counters
|
|
55188
|
-
* say how often that camera loses one and out of how many tries. A second
|
|
55189
|
-
* vocabulary for the same loss would break the join silently — the row would
|
|
55190
|
-
* say `native-miss` and the counter `nativeMiss`, and nobody would notice until
|
|
55191
|
-
* somebody tried to correlate them. So the reasons live here, named once, and
|
|
55192
|
-
* they deliberately reuse D276's spelling.
|
|
55193
|
-
*
|
|
55194
|
-
* ## Every family carries its denominator
|
|
55195
|
-
*
|
|
55196
|
-
* Each `note*` call is ONE attempt. The success calls are not decoration: a
|
|
55197
|
-
* failure count without them is the number that read as "35x worse than
|
|
55198
|
-
* yesterday" on 2026-08-28 and was flat all day once divided
|
|
55199
|
-
* (`packages/types/src/metrics/failure-counters.ts`).
|
|
55200
|
-
*
|
|
55201
|
-
* ## Nothing here logs
|
|
55202
|
-
*
|
|
55203
|
-
* These paths run at frame rate — 507 enrichment-crop misses in five minutes at
|
|
55204
|
-
* peak, ~100/min. A line each is its own defect, and the log lines that already
|
|
55205
|
-
* exist on these paths (`enrichment crop native miss`, `capture queue full`,
|
|
55206
|
-
* `plate: read discarded`) are unchanged and still throttled. This is the
|
|
55207
|
-
* counter that makes them countable without reading them.
|
|
55208
|
-
*/
|
|
55209
|
-
/**
|
|
55210
|
-
* The enrichment crop capture — face / plate / clip / detail-classifier.
|
|
55211
|
-
* Denominator: every crop this addon asked the owning runner for.
|
|
55212
|
-
*/
|
|
55213
|
-
var FAMILY_ENRICHMENT_CROP = "enrichment-crop";
|
|
55214
|
-
/**
|
|
55215
|
-
* The per-camera capture scheduler lane. Denominator: every queued capture that
|
|
55216
|
-
* reached a TERMINAL scheduling outcome — started, or dropped. A coalesced or
|
|
55217
|
-
* cancelled request is neither and is deliberately not counted: a track that
|
|
55218
|
-
* closed before its capture ran did not lose work, it stopped wanting it.
|
|
55219
|
-
*/
|
|
55220
|
-
var FAMILY_CAPTURE_SCHEDULE = "capture-schedule";
|
|
55221
|
-
/** Plate OCR reads routed from the detail plane. Denominator: every read. */
|
|
55222
|
-
var FAMILY_PLATE_OCR = "plate-ocr";
|
|
55223
|
-
/**
|
|
55224
|
-
* Person tracks born on this camera. Denominator: every confirmed person birth.
|
|
55225
|
-
*
|
|
55226
|
-
* The "failure" here is NOT suppressed and must not be — a person standing over
|
|
55227
|
-
* a car is a real person, and this repo will not trade that away for a cleaner
|
|
55228
|
-
* count (D275 made the same choice for stationary suppression). It is counted so
|
|
55229
|
-
* the phantom rate is a number instead of an impression: 4 of 23 on camera 617
|
|
55230
|
-
* in two hours, measured by hand on 2026-08-28.
|
|
55231
|
-
*/
|
|
55232
|
-
var FAMILY_PERSON_BIRTH = "person-birth";
|
|
55233
|
-
/**
|
|
55234
|
-
* The owning runner had no native pixels for the ROI.
|
|
55235
|
-
*
|
|
55236
|
-
* This collapses the runner-side causes the operator can already see in logs
|
|
55237
|
-
* (`worker-lease-gone`, `no-registry-entry`, `jpeg-encode-failed`) because
|
|
55238
|
-
* `pipelineRunner.getNativeCrop` answers an ROI miss with `null` and carries no
|
|
55239
|
-
* reason on the wire — and `FrameHandle` has no `deviceId`, so the runner could
|
|
55240
|
-
* not attribute the split per camera even if it reported it. Splitting this is a
|
|
55241
|
-
* framework change (a reason on the reply); until then the honest name is the
|
|
55242
|
-
* one the caller can actually stand behind.
|
|
55243
|
-
*/
|
|
55244
|
-
var REASON_NATIVE_MISS = "native-miss";
|
|
55245
|
-
/** Queued past `staleAfterMs`; the frame it wanted is gone. */
|
|
55246
|
-
var REASON_STALE_DROP = "stale-drop";
|
|
55247
|
-
/** Shed because the camera's lane was at its queue cap. */
|
|
55248
|
-
var REASON_QUEUE_OVERFLOW = "queue-overflow";
|
|
55249
|
-
/**
|
|
55250
|
-
* The OCR step returned, with nothing in it.
|
|
55251
|
-
*
|
|
55252
|
-
* The defect this counter was built for: ~35% of plate reads came back with an
|
|
55253
|
-
* empty label and fell through the detail router's `d.label.length > 0` gate
|
|
55254
|
-
* into no branch at all — no store, no log, no row. Silence read as "this camera
|
|
55255
|
-
* never saw a plate", which is a completely different fix.
|
|
55256
|
-
*/
|
|
55257
|
-
var REASON_EMPTY_READ = "empty-read";
|
|
55258
|
-
/** A plate detail with no bbox — nothing to crop, nothing to place. */
|
|
55259
|
-
var REASON_NO_BBOX = "no-bbox";
|
|
55260
|
-
/** `isPlausiblePlateRead` refused it (junk OCR off a distant/oblique plate). */
|
|
55261
|
-
var REASON_IMPLAUSIBLE = "implausible";
|
|
55262
|
-
/** Below the camera's configured minimum OCR confidence. */
|
|
55263
|
-
var REASON_BELOW_MIN_CONFIDENCE = "below-min-confidence";
|
|
55264
|
-
/** A person born with its box inside a vehicle's. See {@link FAMILY_PERSON_BIRTH}. */
|
|
55265
|
-
var REASON_PERSON_OVER_VEHICLE = "person-over-vehicle";
|
|
55266
|
-
/**
|
|
55267
|
-
* This addon's `failure-contribution` counters.
|
|
55268
|
-
*
|
|
55269
|
-
* One instance per addon process, handed to the paths that fail. Every method
|
|
55270
|
-
* takes the numeric `deviceId` first, for the same reason every log line does:
|
|
55271
|
-
* the question is always per camera.
|
|
55272
|
-
*/
|
|
55273
|
-
var AnalyticsFailureReport = class {
|
|
55274
|
-
now;
|
|
55275
|
-
counters;
|
|
55276
|
-
constructor(now = Date.now, counters = new FailureCounters()) {
|
|
55277
|
-
this.now = now;
|
|
55278
|
-
this.counters = counters;
|
|
55279
|
-
}
|
|
55280
|
-
/** One enrichment crop capture. `reason` absent = the native ROI landed. */
|
|
55281
|
-
noteEnrichmentCrop(deviceId, reason) {
|
|
55282
|
-
this.counters.note({
|
|
55283
|
-
deviceId,
|
|
55284
|
-
family: FAMILY_ENRICHMENT_CROP,
|
|
55285
|
-
...reason !== void 0 ? { reason } : {}
|
|
55286
|
-
}, this.now());
|
|
55287
|
-
}
|
|
55288
|
-
/** One capture that reached a terminal scheduling outcome. */
|
|
55289
|
-
noteCaptureSchedule(deviceId, reason) {
|
|
55290
|
-
this.counters.note({
|
|
55291
|
-
deviceId,
|
|
55292
|
-
family: FAMILY_CAPTURE_SCHEDULE,
|
|
55293
|
-
...reason !== void 0 ? { reason } : {}
|
|
55294
|
-
}, this.now());
|
|
55295
|
-
}
|
|
55296
|
-
/** One plate OCR read. `reason` absent = the read was accepted and stored. */
|
|
55297
|
-
notePlateRead(deviceId, reason) {
|
|
55298
|
-
this.counters.note({
|
|
55299
|
-
deviceId,
|
|
55300
|
-
family: FAMILY_PLATE_OCR,
|
|
55301
|
-
...reason !== void 0 ? { reason } : {}
|
|
55302
|
-
}, this.now());
|
|
55303
|
-
}
|
|
55304
|
-
/** One confirmed person birth. `reason` present = it was born over a vehicle. */
|
|
55305
|
-
notePersonBirth(deviceId, reason) {
|
|
55306
|
-
this.counters.note({
|
|
55307
|
-
deviceId,
|
|
55308
|
-
family: FAMILY_PERSON_BIRTH,
|
|
55309
|
-
...reason !== void 0 ? { reason } : {}
|
|
55310
|
-
}, this.now());
|
|
55311
|
-
}
|
|
55312
|
-
/** The `failure-contribution` provider's payload. Reads, never resets. */
|
|
55313
|
-
list() {
|
|
55314
|
-
return this.counters.snapshot(this.now());
|
|
55315
|
-
}
|
|
55316
|
-
/** Addon disposal. */
|
|
55317
|
-
clear() {
|
|
55318
|
-
this.counters.clear();
|
|
55319
|
-
}
|
|
55320
|
-
};
|
|
55321
|
-
//#endregion
|
|
55322
55496
|
//#region src/pipeline-analytics/plate-recognizer.ts
|
|
55323
55497
|
/**
|
|
55324
55498
|
* PlateRecognizer — collects EVERY license-plate OCR read a vehicle track
|
|
@@ -60377,31 +60551,6 @@ var NativeCropDedupe = class {
|
|
|
60377
60551
|
}
|
|
60378
60552
|
};
|
|
60379
60553
|
//#endregion
|
|
60380
|
-
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
60381
|
-
function createCaptureCrop(deps) {
|
|
60382
|
-
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
60383
|
-
const paddedNorm = padBbox({
|
|
60384
|
-
x: bbox.x / frameWidth,
|
|
60385
|
-
y: bbox.y / frameHeight,
|
|
60386
|
-
w: bbox.w / frameWidth,
|
|
60387
|
-
h: bbox.h / frameHeight
|
|
60388
|
-
}, padding);
|
|
60389
|
-
const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
60390
|
-
if (nativeCrop) {
|
|
60391
|
-
deps.bumpCropMetric(true);
|
|
60392
|
-
if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId);
|
|
60393
|
-
return nativeCrop;
|
|
60394
|
-
}
|
|
60395
|
-
deps.bumpCropMetric(false);
|
|
60396
|
-
if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId, REASON_NATIVE_MISS);
|
|
60397
|
-
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
|
|
60398
|
-
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
60399
|
-
meta: { nodeId: frameHandle.nodeId }
|
|
60400
|
-
});
|
|
60401
|
-
return null;
|
|
60402
|
-
};
|
|
60403
|
-
}
|
|
60404
|
-
//#endregion
|
|
60405
60554
|
//#region src/pipeline-analytics/pipeline/display-crop.ts
|
|
60406
60555
|
/** Bound the once-per-owner memo so a long-lived process never leaks it. */
|
|
60407
60556
|
var MAX_LOGGED_OWNERS = 5e3;
|
|
@@ -65277,6 +65426,144 @@ function rowFromMetadata(id, metadata) {
|
|
|
65277
65426
|
};
|
|
65278
65427
|
}
|
|
65279
65428
|
//#endregion
|
|
65429
|
+
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
65430
|
+
/** How long a mirrored entry is served before a refresh is started behind the
|
|
65431
|
+
* next reader. See the module doc for why this matches the per-kind caches'
|
|
65432
|
+
* own window rather than introducing a second staleness bound. */
|
|
65433
|
+
var RAW_SETTINGS_STALE_MS = 5e3;
|
|
65434
|
+
/**
|
|
65435
|
+
* How long a FAILED refresh is not retried.
|
|
65436
|
+
*
|
|
65437
|
+
* A refresh that throws leaves `fetchedAt` where it was, so without a
|
|
65438
|
+
* cooldown every subsequent reader would start another attempt — a broken
|
|
65439
|
+
* settings store would be hammered at frame rate by the very mechanism built
|
|
65440
|
+
* to stop that.
|
|
65441
|
+
*/
|
|
65442
|
+
var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
|
|
65443
|
+
/**
|
|
65444
|
+
* The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
|
|
65445
|
+
* fresh OFF every per-kind settings cache's hot path.
|
|
65446
|
+
*
|
|
65447
|
+
* `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
|
|
65448
|
+
* (or frames) missing on the same device in the same tick costs ONE read,
|
|
65449
|
+
* not one per caller — the exact failure this class exists to remove.
|
|
65450
|
+
*/
|
|
65451
|
+
var RawDeviceBlobMirror = class {
|
|
65452
|
+
deps;
|
|
65453
|
+
staleMs;
|
|
65454
|
+
missCooldownMs;
|
|
65455
|
+
entries = /* @__PURE__ */ new Map();
|
|
65456
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
65457
|
+
now;
|
|
65458
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
65459
|
+
this.deps = deps;
|
|
65460
|
+
this.staleMs = staleMs;
|
|
65461
|
+
this.missCooldownMs = missCooldownMs;
|
|
65462
|
+
this.now = deps.now ?? (() => Date.now());
|
|
65463
|
+
}
|
|
65464
|
+
/**
|
|
65465
|
+
* The device's raw stored blob.
|
|
65466
|
+
*
|
|
65467
|
+
* COLD awaits — the first settings kind to touch a device has to get the
|
|
65468
|
+
* real answer. WARM returns immediately, stale or not, and starts the
|
|
65469
|
+
* refresh behind the caller.
|
|
65470
|
+
*/
|
|
65471
|
+
async resolve(deviceId) {
|
|
65472
|
+
const entry = this.entries.get(deviceId);
|
|
65473
|
+
if (entry === void 0) return this.load(deviceId);
|
|
65474
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
|
|
65475
|
+
return entry.value;
|
|
65476
|
+
}
|
|
65477
|
+
/** Drop a device's entry. Called by every in-process writer of this
|
|
65478
|
+
* device's row, so an operator's own change is never served stale. */
|
|
65479
|
+
invalidate(deviceId) {
|
|
65480
|
+
this.entries.delete(deviceId);
|
|
65481
|
+
}
|
|
65482
|
+
invalidateAll() {
|
|
65483
|
+
this.entries.clear();
|
|
65484
|
+
}
|
|
65485
|
+
async load(deviceId) {
|
|
65486
|
+
const pending = this.inFlight.get(deviceId);
|
|
65487
|
+
if (pending !== void 0) return pending;
|
|
65488
|
+
const run = this.deps.read(deviceId).then((value) => {
|
|
65489
|
+
this.entries.set(deviceId, {
|
|
65490
|
+
value,
|
|
65491
|
+
fetchedAt: this.now()
|
|
65492
|
+
});
|
|
65493
|
+
return value;
|
|
65494
|
+
}).catch((err) => {
|
|
65495
|
+
this.deps.onRefreshFailed?.(deviceId, err);
|
|
65496
|
+
const existing = this.entries.get(deviceId);
|
|
65497
|
+
if (existing !== void 0) {
|
|
65498
|
+
existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
|
|
65499
|
+
return existing.value;
|
|
65500
|
+
}
|
|
65501
|
+
return {};
|
|
65502
|
+
}).finally(() => {
|
|
65503
|
+
this.inFlight.delete(deviceId);
|
|
65504
|
+
});
|
|
65505
|
+
this.inFlight.set(deviceId, run);
|
|
65506
|
+
return run;
|
|
65507
|
+
}
|
|
65508
|
+
};
|
|
65509
|
+
/**
|
|
65510
|
+
* The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
|
|
65511
|
+
* kept fresh OFF every per-kind settings cache's hot path. Same shape as
|
|
65512
|
+
* {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
|
|
65513
|
+
* one addon store per process.
|
|
65514
|
+
*/
|
|
65515
|
+
var RawGlobalStoreMirror = class {
|
|
65516
|
+
deps;
|
|
65517
|
+
staleMs;
|
|
65518
|
+
missCooldownMs;
|
|
65519
|
+
entry = null;
|
|
65520
|
+
inFlight = null;
|
|
65521
|
+
now;
|
|
65522
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
65523
|
+
this.deps = deps;
|
|
65524
|
+
this.staleMs = staleMs;
|
|
65525
|
+
this.missCooldownMs = missCooldownMs;
|
|
65526
|
+
this.now = deps.now ?? (() => Date.now());
|
|
65527
|
+
}
|
|
65528
|
+
async resolve() {
|
|
65529
|
+
const entry = this.entry;
|
|
65530
|
+
if (entry === null) return this.load();
|
|
65531
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
|
|
65532
|
+
return entry.value;
|
|
65533
|
+
}
|
|
65534
|
+
/** Drop the mirrored store. Called by every in-process global-settings
|
|
65535
|
+
* writer (`onConfigChanged`), so an operator's own change is never served
|
|
65536
|
+
* stale. */
|
|
65537
|
+
invalidate() {
|
|
65538
|
+
this.entry = null;
|
|
65539
|
+
}
|
|
65540
|
+
async load() {
|
|
65541
|
+
if (this.inFlight !== null) return this.inFlight;
|
|
65542
|
+
const run = this.deps.read().then((value) => {
|
|
65543
|
+
this.entry = {
|
|
65544
|
+
value,
|
|
65545
|
+
fetchedAt: this.now()
|
|
65546
|
+
};
|
|
65547
|
+
return value;
|
|
65548
|
+
}).catch((err) => {
|
|
65549
|
+
this.deps.onRefreshFailed?.(err);
|
|
65550
|
+
const existing = this.entry;
|
|
65551
|
+
if (existing !== null) {
|
|
65552
|
+
this.entry = {
|
|
65553
|
+
value: existing.value,
|
|
65554
|
+
fetchedAt: this.now() - this.staleMs + this.missCooldownMs
|
|
65555
|
+
};
|
|
65556
|
+
return existing.value;
|
|
65557
|
+
}
|
|
65558
|
+
return {};
|
|
65559
|
+
}).finally(() => {
|
|
65560
|
+
this.inFlight = null;
|
|
65561
|
+
});
|
|
65562
|
+
this.inFlight = run;
|
|
65563
|
+
return run;
|
|
65564
|
+
}
|
|
65565
|
+
};
|
|
65566
|
+
//#endregion
|
|
65280
65567
|
//#region src/pipeline-analytics/summary/summary-score.ts
|
|
65281
65568
|
/** A member whose own box is covered this much by others is hidden. */
|
|
65282
65569
|
var OCCLUDED_COVER_FRACTION = .5;
|
|
@@ -65581,144 +65868,6 @@ var SummarySealer = class {
|
|
|
65581
65868
|
}
|
|
65582
65869
|
};
|
|
65583
65870
|
//#endregion
|
|
65584
|
-
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
65585
|
-
/** How long a mirrored entry is served before a refresh is started behind the
|
|
65586
|
-
* next reader. See the module doc for why this matches the per-kind caches'
|
|
65587
|
-
* own window rather than introducing a second staleness bound. */
|
|
65588
|
-
var RAW_SETTINGS_STALE_MS = 5e3;
|
|
65589
|
-
/**
|
|
65590
|
-
* How long a FAILED refresh is not retried.
|
|
65591
|
-
*
|
|
65592
|
-
* A refresh that throws leaves `fetchedAt` where it was, so without a
|
|
65593
|
-
* cooldown every subsequent reader would start another attempt — a broken
|
|
65594
|
-
* settings store would be hammered at frame rate by the very mechanism built
|
|
65595
|
-
* to stop that.
|
|
65596
|
-
*/
|
|
65597
|
-
var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
|
|
65598
|
-
/**
|
|
65599
|
-
* The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
|
|
65600
|
-
* fresh OFF every per-kind settings cache's hot path.
|
|
65601
|
-
*
|
|
65602
|
-
* `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
|
|
65603
|
-
* (or frames) missing on the same device in the same tick costs ONE read,
|
|
65604
|
-
* not one per caller — the exact failure this class exists to remove.
|
|
65605
|
-
*/
|
|
65606
|
-
var RawDeviceBlobMirror = class {
|
|
65607
|
-
deps;
|
|
65608
|
-
staleMs;
|
|
65609
|
-
missCooldownMs;
|
|
65610
|
-
entries = /* @__PURE__ */ new Map();
|
|
65611
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
65612
|
-
now;
|
|
65613
|
-
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
65614
|
-
this.deps = deps;
|
|
65615
|
-
this.staleMs = staleMs;
|
|
65616
|
-
this.missCooldownMs = missCooldownMs;
|
|
65617
|
-
this.now = deps.now ?? (() => Date.now());
|
|
65618
|
-
}
|
|
65619
|
-
/**
|
|
65620
|
-
* The device's raw stored blob.
|
|
65621
|
-
*
|
|
65622
|
-
* COLD awaits — the first settings kind to touch a device has to get the
|
|
65623
|
-
* real answer. WARM returns immediately, stale or not, and starts the
|
|
65624
|
-
* refresh behind the caller.
|
|
65625
|
-
*/
|
|
65626
|
-
async resolve(deviceId) {
|
|
65627
|
-
const entry = this.entries.get(deviceId);
|
|
65628
|
-
if (entry === void 0) return this.load(deviceId);
|
|
65629
|
-
if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
|
|
65630
|
-
return entry.value;
|
|
65631
|
-
}
|
|
65632
|
-
/** Drop a device's entry. Called by every in-process writer of this
|
|
65633
|
-
* device's row, so an operator's own change is never served stale. */
|
|
65634
|
-
invalidate(deviceId) {
|
|
65635
|
-
this.entries.delete(deviceId);
|
|
65636
|
-
}
|
|
65637
|
-
invalidateAll() {
|
|
65638
|
-
this.entries.clear();
|
|
65639
|
-
}
|
|
65640
|
-
async load(deviceId) {
|
|
65641
|
-
const pending = this.inFlight.get(deviceId);
|
|
65642
|
-
if (pending !== void 0) return pending;
|
|
65643
|
-
const run = this.deps.read(deviceId).then((value) => {
|
|
65644
|
-
this.entries.set(deviceId, {
|
|
65645
|
-
value,
|
|
65646
|
-
fetchedAt: this.now()
|
|
65647
|
-
});
|
|
65648
|
-
return value;
|
|
65649
|
-
}).catch((err) => {
|
|
65650
|
-
this.deps.onRefreshFailed?.(deviceId, err);
|
|
65651
|
-
const existing = this.entries.get(deviceId);
|
|
65652
|
-
if (existing !== void 0) {
|
|
65653
|
-
existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
|
|
65654
|
-
return existing.value;
|
|
65655
|
-
}
|
|
65656
|
-
return {};
|
|
65657
|
-
}).finally(() => {
|
|
65658
|
-
this.inFlight.delete(deviceId);
|
|
65659
|
-
});
|
|
65660
|
-
this.inFlight.set(deviceId, run);
|
|
65661
|
-
return run;
|
|
65662
|
-
}
|
|
65663
|
-
};
|
|
65664
|
-
/**
|
|
65665
|
-
* The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
|
|
65666
|
-
* kept fresh OFF every per-kind settings cache's hot path. Same shape as
|
|
65667
|
-
* {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
|
|
65668
|
-
* one addon store per process.
|
|
65669
|
-
*/
|
|
65670
|
-
var RawGlobalStoreMirror = class {
|
|
65671
|
-
deps;
|
|
65672
|
-
staleMs;
|
|
65673
|
-
missCooldownMs;
|
|
65674
|
-
entry = null;
|
|
65675
|
-
inFlight = null;
|
|
65676
|
-
now;
|
|
65677
|
-
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
65678
|
-
this.deps = deps;
|
|
65679
|
-
this.staleMs = staleMs;
|
|
65680
|
-
this.missCooldownMs = missCooldownMs;
|
|
65681
|
-
this.now = deps.now ?? (() => Date.now());
|
|
65682
|
-
}
|
|
65683
|
-
async resolve() {
|
|
65684
|
-
const entry = this.entry;
|
|
65685
|
-
if (entry === null) return this.load();
|
|
65686
|
-
if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
|
|
65687
|
-
return entry.value;
|
|
65688
|
-
}
|
|
65689
|
-
/** Drop the mirrored store. Called by every in-process global-settings
|
|
65690
|
-
* writer (`onConfigChanged`), so an operator's own change is never served
|
|
65691
|
-
* stale. */
|
|
65692
|
-
invalidate() {
|
|
65693
|
-
this.entry = null;
|
|
65694
|
-
}
|
|
65695
|
-
async load() {
|
|
65696
|
-
if (this.inFlight !== null) return this.inFlight;
|
|
65697
|
-
const run = this.deps.read().then((value) => {
|
|
65698
|
-
this.entry = {
|
|
65699
|
-
value,
|
|
65700
|
-
fetchedAt: this.now()
|
|
65701
|
-
};
|
|
65702
|
-
return value;
|
|
65703
|
-
}).catch((err) => {
|
|
65704
|
-
this.deps.onRefreshFailed?.(err);
|
|
65705
|
-
const existing = this.entry;
|
|
65706
|
-
if (existing !== null) {
|
|
65707
|
-
this.entry = {
|
|
65708
|
-
value: existing.value,
|
|
65709
|
-
fetchedAt: this.now() - this.staleMs + this.missCooldownMs
|
|
65710
|
-
};
|
|
65711
|
-
return existing.value;
|
|
65712
|
-
}
|
|
65713
|
-
return {};
|
|
65714
|
-
}).finally(() => {
|
|
65715
|
-
this.inFlight = null;
|
|
65716
|
-
});
|
|
65717
|
-
this.inFlight = run;
|
|
65718
|
-
return run;
|
|
65719
|
-
}
|
|
65720
|
-
};
|
|
65721
|
-
//#endregion
|
|
65722
65871
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
65723
65872
|
/**
|
|
65724
65873
|
* Sweep every device with persisted tracks: skip retention-disabled devices,
|
|
@@ -67306,6 +67455,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
67306
67455
|
suppressedBirths = new SuppressedBirthRegistry();
|
|
67307
67456
|
/** Births the gate could not MEASURE, awaiting another look on a later frame. */
|
|
67308
67457
|
deferredBirths = new DeferredBirthRegistry();
|
|
67458
|
+
/**
|
|
67459
|
+
* Subjects the confirmation gate found in a birth's crop BESIDES the birth
|
|
67460
|
+
* itself (a dog in a person's crop), waiting to ride the next frame of the
|
|
67461
|
+
* same (device, source) as detections — `pipeline/secondary-subjects.ts`.
|
|
67462
|
+
*/
|
|
67463
|
+
secondarySubjects = new SecondarySubjectQueue();
|
|
67309
67464
|
lastFrameDimsByDevice = /* @__PURE__ */ new Map();
|
|
67310
67465
|
lastAudioInsertByDevice = /* @__PURE__ */ new Map();
|
|
67311
67466
|
lastMotionInsertByDevice = /* @__PURE__ */ new Map();
|
|
@@ -70402,9 +70557,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
70402
70557
|
processor.setZones(liveZones);
|
|
70403
70558
|
processor.setDetectionRules(liveRules);
|
|
70404
70559
|
processor.setZoneMembershipMinOverlap(trk.zoneMembershipMinOverlap);
|
|
70560
|
+
const injection = injectSecondarySubjects(frame, this.secondarySubjects.take(key, Date.now()));
|
|
70561
|
+
for (const id of injection.covered) this.secondarySubjects.drop(key, id);
|
|
70562
|
+
const trackerFrame = injection.frame;
|
|
70563
|
+
if (trackerFrame !== frame) this.ctx.logger.info("secondary subjects ride this frame as detections", {
|
|
70564
|
+
tags: { deviceId },
|
|
70565
|
+
meta: {
|
|
70566
|
+
source,
|
|
70567
|
+
added: trackerFrame.detections.length - frame.detections.length,
|
|
70568
|
+
classes: trackerFrame.detections.slice(frame.detections.length).map((d) => d.macroClass)
|
|
70569
|
+
}
|
|
70570
|
+
});
|
|
70405
70571
|
const result = processor.process({
|
|
70406
70572
|
timestamp: frame.timestamp,
|
|
70407
|
-
frame
|
|
70573
|
+
frame: trackerFrame
|
|
70408
70574
|
});
|
|
70409
70575
|
for (const r of processor.getLastLabelWriteRefusals()) {
|
|
70410
70576
|
const message = r.reason.kind === "macro-class" ? "label tier collapse refused" : "label write refused";
|
|
@@ -70547,6 +70713,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
70547
70713
|
bbox: t.bbox
|
|
70548
70714
|
})), {
|
|
70549
70715
|
deviceId,
|
|
70716
|
+
source,
|
|
70550
70717
|
frameHandle,
|
|
70551
70718
|
frameWidth: result.frameWidth,
|
|
70552
70719
|
frameHeight: result.frameHeight
|
|
@@ -71191,16 +71358,25 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71191
71358
|
const executor = this.pipelineExecutorApi();
|
|
71192
71359
|
if (!executor?.runPipeline) return null;
|
|
71193
71360
|
try {
|
|
71194
|
-
const
|
|
71361
|
+
const result = await executor.runPipeline.mutate({
|
|
71195
71362
|
steps: [step],
|
|
71196
71363
|
image: new Uint8Array(cropJpeg),
|
|
71197
71364
|
deviceId,
|
|
71198
71365
|
plane: "frame"
|
|
71199
|
-
}, nodePin(nodeId))
|
|
71366
|
+
}, nodePin(nodeId));
|
|
71367
|
+
const detections = result?.detections;
|
|
71200
71368
|
if (!detections) return null;
|
|
71369
|
+
const cropWidth = result.width ?? 0;
|
|
71370
|
+
const cropHeight = result.height ?? 0;
|
|
71201
71371
|
return detections.map((d) => ({
|
|
71202
71372
|
macroClass: d.macroClass,
|
|
71203
|
-
score: d.score
|
|
71373
|
+
score: d.score,
|
|
71374
|
+
...d.bbox !== void 0 && cropWidth > 0 && cropHeight > 0 ? { bbox: {
|
|
71375
|
+
x: d.bbox.x / cropWidth,
|
|
71376
|
+
y: d.bbox.y / cropHeight,
|
|
71377
|
+
w: d.bbox.width / cropWidth,
|
|
71378
|
+
h: d.bbox.height / cropHeight
|
|
71379
|
+
} } : {}
|
|
71204
71380
|
}));
|
|
71205
71381
|
} catch (err) {
|
|
71206
71382
|
this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
|
|
@@ -71226,7 +71402,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71226
71402
|
if (candidates.length === 0) return allConfirmed();
|
|
71227
71403
|
const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
|
|
71228
71404
|
if (!config.enabled) return allConfirmed();
|
|
71229
|
-
const { frameHandle, frameWidth, frameHeight, deviceId } = params;
|
|
71405
|
+
const { frameHandle, frameWidth, frameHeight, deviceId, source } = params;
|
|
71230
71406
|
const captureCrop = this.captureCrop;
|
|
71231
71407
|
if (!frameHandle || !captureCrop || frameWidth <= 0 || frameHeight <= 0) {
|
|
71232
71408
|
this.ctx.logger.info("confirmation gate: no crop path — births allowed unmeasured", {
|
|
@@ -71266,6 +71442,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71266
71442
|
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, 320);
|
|
71267
71443
|
},
|
|
71268
71444
|
redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
|
|
71445
|
+
cropRectFor: (candidate) => captureCropFrameRect({
|
|
71446
|
+
x: candidate.bbox.x,
|
|
71447
|
+
y: candidate.bbox.y,
|
|
71448
|
+
w: candidate.bbox.w,
|
|
71449
|
+
h: candidate.bbox.h
|
|
71450
|
+
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING),
|
|
71269
71451
|
minConfidenceFor: (candidate) => this.phantomCells?.spawnBar({
|
|
71270
71452
|
deviceId,
|
|
71271
71453
|
className: candidate.className,
|
|
@@ -71310,6 +71492,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71310
71492
|
});
|
|
71311
71493
|
break;
|
|
71312
71494
|
}
|
|
71495
|
+
if (decision.secondary !== void 0) {
|
|
71496
|
+
this.secondarySubjects.add(this.procKey(deviceId, source), decision.secondary, decision.trackId, Date.now());
|
|
71497
|
+
this.ctx.logger.info("confirmation gate: secondary subject promoted from the crop", {
|
|
71498
|
+
tags: { deviceId },
|
|
71499
|
+
meta: {
|
|
71500
|
+
trackId: decision.trackId,
|
|
71501
|
+
className: decision.className,
|
|
71502
|
+
secondaryClass: decision.secondary.className,
|
|
71503
|
+
secondaryScore: decision.secondary.score,
|
|
71504
|
+
secondaryBbox: decision.secondary.bbox,
|
|
71505
|
+
verdict: decision.verdict
|
|
71506
|
+
}
|
|
71507
|
+
});
|
|
71508
|
+
}
|
|
71313
71509
|
}
|
|
71314
71510
|
}, exhaustedIds);
|
|
71315
71511
|
}
|
|
@@ -73186,6 +73382,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73186
73382
|
const prefix = `${deviceId}:`;
|
|
73187
73383
|
for (const k of this.processors.keys()) if (k.startsWith(prefix)) this.processors.delete(k);
|
|
73188
73384
|
for (const k of this.lastActiveTrackIds.keys()) if (k.startsWith(prefix)) this.lastActiveTrackIds.delete(k);
|
|
73385
|
+
for (const source of ["pipeline", "onboard"]) this.secondarySubjects.forget(this.procKey(deviceId, source));
|
|
73189
73386
|
for (const k of this.dropoutSkipsByKey.keys()) if (k.startsWith(prefix)) this.dropoutSkipsByKey.delete(k);
|
|
73190
73387
|
this.lastFrameDimsByDevice.delete(deviceId);
|
|
73191
73388
|
}
|