@camstack/addon-post-analysis 1.2.190 → 1.2.192
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-Gc-d7G7c.mjs} +3 -3
- 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-Bprfn5kJ.mjs} +3 -3
- 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
|
@@ -33278,6 +33278,52 @@ var ClosedRowMediaGate = class {
|
|
|
33278
33278
|
}
|
|
33279
33279
|
};
|
|
33280
33280
|
//#endregion
|
|
33281
|
+
//#region src/pipeline-analytics/pipeline/co-moving-subjects.ts
|
|
33282
|
+
/**
|
|
33283
|
+
* Report at this much containment. A rider and a bicycle overlap heavily; two
|
|
33284
|
+
* people passing each other in the frame do not stay contained for long.
|
|
33285
|
+
* Deliberately a REPORTING threshold, not a decision one — nothing branches on
|
|
33286
|
+
* it, so it can be loose enough to see the near misses.
|
|
33287
|
+
*/
|
|
33288
|
+
var CO_MOVING_MIN_CONTAINMENT = .6;
|
|
33289
|
+
function intersectionArea$4(a, b) {
|
|
33290
|
+
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));
|
|
33291
|
+
}
|
|
33292
|
+
/**
|
|
33293
|
+
* The overlaps a NEWLY born track has with live tracks of a DIFFERENT class.
|
|
33294
|
+
*
|
|
33295
|
+
* Same-class overlaps are excluded: those are the tracker's own duplicate
|
|
33296
|
+
* problem and are already measured elsewhere. A zero-area box reports nothing
|
|
33297
|
+
* rather than dividing by zero.
|
|
33298
|
+
*/
|
|
33299
|
+
function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMENT) {
|
|
33300
|
+
const out = [];
|
|
33301
|
+
for (const b of born) {
|
|
33302
|
+
const areaB = b.bbox.w * b.bbox.h;
|
|
33303
|
+
if (areaB <= 0) continue;
|
|
33304
|
+
for (const o of live) {
|
|
33305
|
+
if (o.trackId === b.trackId) continue;
|
|
33306
|
+
if (o.className === b.className) continue;
|
|
33307
|
+
const areaO = o.bbox.w * o.bbox.h;
|
|
33308
|
+
if (areaO <= 0) continue;
|
|
33309
|
+
const inter = intersectionArea$4(b.bbox, o.bbox);
|
|
33310
|
+
if (inter <= 0) continue;
|
|
33311
|
+
const union = areaB + areaO - inter;
|
|
33312
|
+
const containment = inter / Math.min(areaB, areaO);
|
|
33313
|
+
if (containment < minContainment) continue;
|
|
33314
|
+
out.push({
|
|
33315
|
+
trackId: b.trackId,
|
|
33316
|
+
className: b.className,
|
|
33317
|
+
otherTrackId: o.trackId,
|
|
33318
|
+
otherClassName: o.className,
|
|
33319
|
+
iou: union > 0 ? inter / union : 0,
|
|
33320
|
+
containment
|
|
33321
|
+
});
|
|
33322
|
+
}
|
|
33323
|
+
}
|
|
33324
|
+
return out;
|
|
33325
|
+
}
|
|
33326
|
+
//#endregion
|
|
33281
33327
|
//#region src/pipeline-analytics/pipeline/edge-clear.ts
|
|
33282
33328
|
/**
|
|
33283
33329
|
* edge-clear — pure geometry for "is the subject fully in frame?" best-frame
|
|
@@ -33498,52 +33544,6 @@ function isEdgeAwareNewBest(current, candidate, hysteresis) {
|
|
|
33498
33544
|
return false;
|
|
33499
33545
|
}
|
|
33500
33546
|
//#endregion
|
|
33501
|
-
//#region src/pipeline-analytics/pipeline/co-moving-subjects.ts
|
|
33502
|
-
/**
|
|
33503
|
-
* Report at this much containment. A rider and a bicycle overlap heavily; two
|
|
33504
|
-
* people passing each other in the frame do not stay contained for long.
|
|
33505
|
-
* Deliberately a REPORTING threshold, not a decision one — nothing branches on
|
|
33506
|
-
* it, so it can be loose enough to see the near misses.
|
|
33507
|
-
*/
|
|
33508
|
-
var CO_MOVING_MIN_CONTAINMENT = .6;
|
|
33509
|
-
function intersectionArea$4(a, b) {
|
|
33510
|
-
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));
|
|
33511
|
-
}
|
|
33512
|
-
/**
|
|
33513
|
-
* The overlaps a NEWLY born track has with live tracks of a DIFFERENT class.
|
|
33514
|
-
*
|
|
33515
|
-
* Same-class overlaps are excluded: those are the tracker's own duplicate
|
|
33516
|
-
* problem and are already measured elsewhere. A zero-area box reports nothing
|
|
33517
|
-
* rather than dividing by zero.
|
|
33518
|
-
*/
|
|
33519
|
-
function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMENT) {
|
|
33520
|
-
const out = [];
|
|
33521
|
-
for (const b of born) {
|
|
33522
|
-
const areaB = b.bbox.w * b.bbox.h;
|
|
33523
|
-
if (areaB <= 0) continue;
|
|
33524
|
-
for (const o of live) {
|
|
33525
|
-
if (o.trackId === b.trackId) continue;
|
|
33526
|
-
if (o.className === b.className) continue;
|
|
33527
|
-
const areaO = o.bbox.w * o.bbox.h;
|
|
33528
|
-
if (areaO <= 0) continue;
|
|
33529
|
-
const inter = intersectionArea$4(b.bbox, o.bbox);
|
|
33530
|
-
if (inter <= 0) continue;
|
|
33531
|
-
const union = areaB + areaO - inter;
|
|
33532
|
-
const containment = inter / Math.min(areaB, areaO);
|
|
33533
|
-
if (containment < minContainment) continue;
|
|
33534
|
-
out.push({
|
|
33535
|
-
trackId: b.trackId,
|
|
33536
|
-
className: b.className,
|
|
33537
|
-
otherTrackId: o.trackId,
|
|
33538
|
-
otherClassName: o.className,
|
|
33539
|
-
iou: union > 0 ? inter / union : 0,
|
|
33540
|
-
containment
|
|
33541
|
-
});
|
|
33542
|
-
}
|
|
33543
|
-
}
|
|
33544
|
-
return out;
|
|
33545
|
-
}
|
|
33546
|
-
//#endregion
|
|
33547
33547
|
//#region src/pipeline-analytics/pipeline/first-frame-retry-bbox.ts
|
|
33548
33548
|
/** Crop box for a late firstFrame retry: the BIRTH geometry, not the
|
|
33549
33549
|
* later frozen/coasted box (track 1b659d56). */
|
|
@@ -34067,7 +34067,7 @@ var MAX_PATH_LENGTH = 300;
|
|
|
34067
34067
|
function clamp(value, min, max) {
|
|
34068
34068
|
return Math.max(min, Math.min(max, value));
|
|
34069
34069
|
}
|
|
34070
|
-
function iou$
|
|
34070
|
+
function iou$3(a, b) {
|
|
34071
34071
|
const ax1 = a.x, ay1 = a.y, ax2 = a.x + a.w, ay2 = a.y + a.h;
|
|
34072
34072
|
const bx1 = b.x, by1 = b.y, bx2 = b.x + b.w, by2 = b.y + b.h;
|
|
34073
34073
|
const ix1 = Math.max(ax1, bx1), iy1 = Math.max(ay1, by1);
|
|
@@ -34135,7 +34135,7 @@ var SortTracker = class SortTracker {
|
|
|
34135
34135
|
trackClass: track.class,
|
|
34136
34136
|
detClass: det.class,
|
|
34137
34137
|
detScore: det.score,
|
|
34138
|
-
iou: iou$
|
|
34138
|
+
iou: iou$3(track.bbox, det.bbox),
|
|
34139
34139
|
centroidDistPx: Math.hypot(tc.x - dc.x, tc.y - dc.y),
|
|
34140
34140
|
trackDiagPx: Math.hypot(track.bbox.w, track.bbox.h),
|
|
34141
34141
|
areaRatio: smaller > 0 ? Math.max(trackArea, detArea) / smaller : 0,
|
|
@@ -34199,14 +34199,14 @@ var SortTracker = class SortTracker {
|
|
|
34199
34199
|
isDuplicateSpawn(det, live, timestamp) {
|
|
34200
34200
|
for (const t of live) {
|
|
34201
34201
|
if (!this.dedupCompatible(t.class, det.class)) continue;
|
|
34202
|
-
if (iou$
|
|
34203
|
-
if (growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
|
|
34202
|
+
if (iou$3(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
|
|
34203
|
+
if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
|
|
34204
34204
|
this.reportBind("spawn-dedup", "growth", t, det, timestamp);
|
|
34205
34205
|
return true;
|
|
34206
34206
|
}
|
|
34207
34207
|
}
|
|
34208
34208
|
if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.score < this.config.animalOverPersonMaxScore) {
|
|
34209
|
-
for (const t of live) if (t.class === PERSON_CLASS && iou$
|
|
34209
|
+
for (const t of live) if (t.class === PERSON_CLASS && iou$3(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
|
|
34210
34210
|
}
|
|
34211
34211
|
return false;
|
|
34212
34212
|
}
|
|
@@ -34246,7 +34246,7 @@ var SortTracker = class SortTracker {
|
|
|
34246
34246
|
if (dropIds.has(a.id) || dropIds.has(b.id)) continue;
|
|
34247
34247
|
if (!this.dedupCompatible(a.class, b.class)) continue;
|
|
34248
34248
|
const key = SortTracker.pairKey(a.id, b.id);
|
|
34249
|
-
if (iou$
|
|
34249
|
+
if (iou$3(a.bbox, b.bbox) < this.config.dedupMergeIou) {
|
|
34250
34250
|
this.dupStreaks.delete(key);
|
|
34251
34251
|
continue;
|
|
34252
34252
|
}
|
|
@@ -34322,7 +34322,7 @@ var SortTracker = class SortTracker {
|
|
|
34322
34322
|
*/
|
|
34323
34323
|
looseMatch(track, det, timestamp) {
|
|
34324
34324
|
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
|
|
34325
|
-
if (iou$
|
|
34325
|
+
if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
|
|
34326
34326
|
const tc = bboxCentroid(track.bbox);
|
|
34327
34327
|
const dc = bboxCentroid(det.bbox);
|
|
34328
34328
|
const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
|
|
@@ -34408,7 +34408,7 @@ var SortTracker = class SortTracker {
|
|
|
34408
34408
|
if (used.has(di)) continue;
|
|
34409
34409
|
const det = detections[di];
|
|
34410
34410
|
if (!this.associable(track.class, det.class)) continue;
|
|
34411
|
-
const score = iou$
|
|
34411
|
+
const score = iou$3(pbox, det.bbox);
|
|
34412
34412
|
if (score >= iouThreshold) pairs.push({
|
|
34413
34413
|
track,
|
|
34414
34414
|
detIdx: di,
|
|
@@ -34470,7 +34470,7 @@ var SortTracker = class SortTracker {
|
|
|
34470
34470
|
rescuePairs.push({
|
|
34471
34471
|
track,
|
|
34472
34472
|
detIdx: di,
|
|
34473
|
-
score: iou$
|
|
34473
|
+
score: iou$3(track.bbox, det.bbox),
|
|
34474
34474
|
gate
|
|
34475
34475
|
});
|
|
34476
34476
|
}
|
|
@@ -34547,7 +34547,7 @@ var SortTracker = class SortTracker {
|
|
|
34547
34547
|
const gate = this.looseMatch(lost, det, timestamp);
|
|
34548
34548
|
if (gate === null) continue;
|
|
34549
34549
|
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
|
|
34550
|
-
const score = iou$
|
|
34550
|
+
const score = iou$3(lost.bbox, det.bbox);
|
|
34551
34551
|
const peers = this.sameGroupPeerCount(lost, surviving);
|
|
34552
34552
|
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
34553
34553
|
if (score > refusedScore) {
|
|
@@ -44374,6 +44374,32 @@ var AnalyticsQueryFacade = class {
|
|
|
44374
44374
|
}
|
|
44375
44375
|
};
|
|
44376
44376
|
//#endregion
|
|
44377
|
+
//#region src/pipeline-analytics/audio-confirm-window.ts
|
|
44378
|
+
function emptyAudioConfirmState() {
|
|
44379
|
+
return { hitsByClass: /* @__PURE__ */ new Map() };
|
|
44380
|
+
}
|
|
44381
|
+
function observeAudioConfirm(state, input, cfg) {
|
|
44382
|
+
if (cfg.confirmHits <= 1) return {
|
|
44383
|
+
confirmed: true,
|
|
44384
|
+
nextState: state
|
|
44385
|
+
};
|
|
44386
|
+
const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
|
|
44387
|
+
const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
|
|
44388
|
+
const next = new Map(state.hitsByClass);
|
|
44389
|
+
if (kept.length < cfg.confirmHits) {
|
|
44390
|
+
next.set(input.className, kept);
|
|
44391
|
+
return {
|
|
44392
|
+
confirmed: false,
|
|
44393
|
+
nextState: { hitsByClass: next }
|
|
44394
|
+
};
|
|
44395
|
+
}
|
|
44396
|
+
next.delete(input.className);
|
|
44397
|
+
return {
|
|
44398
|
+
confirmed: true,
|
|
44399
|
+
nextState: { hitsByClass: next }
|
|
44400
|
+
};
|
|
44401
|
+
}
|
|
44402
|
+
//#endregion
|
|
44377
44403
|
//#region src/pipeline-analytics/audio-detection-settings.ts
|
|
44378
44404
|
var AudioDetectionSettingsSchema = require_dist.object({
|
|
44379
44405
|
classificationMinScore: require_dist.number().min(0).max(1).default(.6),
|
|
@@ -44634,32 +44660,6 @@ function observeAudioEpisode(state, sample, knobs) {
|
|
|
44634
44660
|
};
|
|
44635
44661
|
}
|
|
44636
44662
|
//#endregion
|
|
44637
|
-
//#region src/pipeline-analytics/audio-confirm-window.ts
|
|
44638
|
-
function emptyAudioConfirmState() {
|
|
44639
|
-
return { hitsByClass: /* @__PURE__ */ new Map() };
|
|
44640
|
-
}
|
|
44641
|
-
function observeAudioConfirm(state, input, cfg) {
|
|
44642
|
-
if (cfg.confirmHits <= 1) return {
|
|
44643
|
-
confirmed: true,
|
|
44644
|
-
nextState: state
|
|
44645
|
-
};
|
|
44646
|
-
const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
|
|
44647
|
-
const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
|
|
44648
|
-
const next = new Map(state.hitsByClass);
|
|
44649
|
-
if (kept.length < cfg.confirmHits) {
|
|
44650
|
-
next.set(input.className, kept);
|
|
44651
|
-
return {
|
|
44652
|
-
confirmed: false,
|
|
44653
|
-
nextState: { hitsByClass: next }
|
|
44654
|
-
};
|
|
44655
|
-
}
|
|
44656
|
-
next.delete(input.className);
|
|
44657
|
-
return {
|
|
44658
|
-
confirmed: true,
|
|
44659
|
-
nextState: { hitsByClass: next }
|
|
44660
|
-
};
|
|
44661
|
-
}
|
|
44662
|
-
//#endregion
|
|
44663
44663
|
//#region src/pipeline-analytics/audio-event-router.ts
|
|
44664
44664
|
/**
|
|
44665
44665
|
* Decide whether an audio frame is a (meaningful) classification event or a
|
|
@@ -45240,6 +45240,7 @@ async function runConfirmation(candidate, config, deps, exhausted) {
|
|
|
45240
45240
|
} else if (!bestIncompatible || d.score > bestIncompatible.score) bestIncompatible = d;
|
|
45241
45241
|
const bar = Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
|
|
45242
45242
|
const confirmed = best !== void 0 && best.score >= bar;
|
|
45243
|
+
const secondary = bestIncompatible ? promoteSecondary(candidate, bestIncompatible, bar, deps) : null;
|
|
45243
45244
|
return {
|
|
45244
45245
|
trackId: candidate.trackId,
|
|
45245
45246
|
verdict: confirmed ? "confirmed" : "suppressed",
|
|
@@ -45250,7 +45251,30 @@ async function runConfirmation(candidate, config, deps, exhausted) {
|
|
|
45250
45251
|
...bestIncompatible ? {
|
|
45251
45252
|
bestIncompatibleClass: bestIncompatible.macroClass,
|
|
45252
45253
|
bestIncompatibleScore: bestIncompatible.score
|
|
45253
|
-
} : {}
|
|
45254
|
+
} : {},
|
|
45255
|
+
...secondary ? { secondary } : {}
|
|
45256
|
+
};
|
|
45257
|
+
}
|
|
45258
|
+
/**
|
|
45259
|
+
* The incompatible best becomes a secondary subject when it clears the SAME
|
|
45260
|
+
* bar the candidate faced and can be placed in the frame. The crop-normalised
|
|
45261
|
+
* box is scaled into the crop's frame rectangle — the one derivation the
|
|
45262
|
+
* capture used, supplied by the caller, never re-derived here.
|
|
45263
|
+
*/
|
|
45264
|
+
function promoteSecondary(candidate, detection, bar, deps) {
|
|
45265
|
+
if (detection.score < bar || detection.bbox === void 0) return null;
|
|
45266
|
+
const rect = deps.cropRectFor?.(candidate) ?? null;
|
|
45267
|
+
if (rect === null || rect.w <= 0 || rect.h <= 0) return null;
|
|
45268
|
+
const box = detection.bbox;
|
|
45269
|
+
return {
|
|
45270
|
+
className: detection.macroClass,
|
|
45271
|
+
score: detection.score,
|
|
45272
|
+
bbox: {
|
|
45273
|
+
x: rect.x + box.x * rect.w,
|
|
45274
|
+
y: rect.y + box.y * rect.h,
|
|
45275
|
+
w: box.w * rect.w,
|
|
45276
|
+
h: box.h * rect.h
|
|
45277
|
+
}
|
|
45254
45278
|
};
|
|
45255
45279
|
}
|
|
45256
45280
|
async function confirmOne(candidate, config, deps, exhausted) {
|
|
@@ -48010,6 +48034,336 @@ var MediaWriteGate = class {
|
|
|
48010
48034
|
this.paused = false;
|
|
48011
48035
|
}
|
|
48012
48036
|
};
|
|
48037
|
+
//#endregion
|
|
48038
|
+
//#region src/pipeline-analytics/runtime/failure-report.ts
|
|
48039
|
+
/**
|
|
48040
|
+
* What `pipeline-analytics` reports through `failure-contribution` — the four
|
|
48041
|
+
* per-camera counters, and the ONE place their vocabulary is written down.
|
|
48042
|
+
*
|
|
48043
|
+
* ## Why the vocabulary is a module and not four string literals
|
|
48044
|
+
*
|
|
48045
|
+
* D276 gave a track row a `previewMissReason` composed from strings like
|
|
48046
|
+
* `keyframe:native-miss` and `thumbnail:overflow`. Those strings are now
|
|
48047
|
+
* joinable evidence: a row says which track lost its tile, and these counters
|
|
48048
|
+
* say how often that camera loses one and out of how many tries. A second
|
|
48049
|
+
* vocabulary for the same loss would break the join silently — the row would
|
|
48050
|
+
* say `native-miss` and the counter `nativeMiss`, and nobody would notice until
|
|
48051
|
+
* somebody tried to correlate them. So the reasons live here, named once, and
|
|
48052
|
+
* they deliberately reuse D276's spelling.
|
|
48053
|
+
*
|
|
48054
|
+
* ## Every family carries its denominator
|
|
48055
|
+
*
|
|
48056
|
+
* Each `note*` call is ONE attempt. The success calls are not decoration: a
|
|
48057
|
+
* failure count without them is the number that read as "35x worse than
|
|
48058
|
+
* yesterday" on 2026-08-28 and was flat all day once divided
|
|
48059
|
+
* (`packages/types/src/metrics/failure-counters.ts`).
|
|
48060
|
+
*
|
|
48061
|
+
* ## Nothing here logs
|
|
48062
|
+
*
|
|
48063
|
+
* These paths run at frame rate — 507 enrichment-crop misses in five minutes at
|
|
48064
|
+
* peak, ~100/min. A line each is its own defect, and the log lines that already
|
|
48065
|
+
* exist on these paths (`enrichment crop native miss`, `capture queue full`,
|
|
48066
|
+
* `plate: read discarded`) are unchanged and still throttled. This is the
|
|
48067
|
+
* counter that makes them countable without reading them.
|
|
48068
|
+
*/
|
|
48069
|
+
/**
|
|
48070
|
+
* The enrichment crop capture — face / plate / clip / detail-classifier.
|
|
48071
|
+
* Denominator: every crop this addon asked the owning runner for.
|
|
48072
|
+
*/
|
|
48073
|
+
var FAMILY_ENRICHMENT_CROP = "enrichment-crop";
|
|
48074
|
+
/**
|
|
48075
|
+
* The per-camera capture scheduler lane. Denominator: every queued capture that
|
|
48076
|
+
* reached a TERMINAL scheduling outcome — started, or dropped. A coalesced or
|
|
48077
|
+
* cancelled request is neither and is deliberately not counted: a track that
|
|
48078
|
+
* closed before its capture ran did not lose work, it stopped wanting it.
|
|
48079
|
+
*/
|
|
48080
|
+
var FAMILY_CAPTURE_SCHEDULE = "capture-schedule";
|
|
48081
|
+
/** Plate OCR reads routed from the detail plane. Denominator: every read. */
|
|
48082
|
+
var FAMILY_PLATE_OCR = "plate-ocr";
|
|
48083
|
+
/**
|
|
48084
|
+
* Person tracks born on this camera. Denominator: every confirmed person birth.
|
|
48085
|
+
*
|
|
48086
|
+
* The "failure" here is NOT suppressed and must not be — a person standing over
|
|
48087
|
+
* a car is a real person, and this repo will not trade that away for a cleaner
|
|
48088
|
+
* count (D275 made the same choice for stationary suppression). It is counted so
|
|
48089
|
+
* the phantom rate is a number instead of an impression: 4 of 23 on camera 617
|
|
48090
|
+
* in two hours, measured by hand on 2026-08-28.
|
|
48091
|
+
*/
|
|
48092
|
+
var FAMILY_PERSON_BIRTH = "person-birth";
|
|
48093
|
+
/**
|
|
48094
|
+
* The owning runner had no native pixels for the ROI.
|
|
48095
|
+
*
|
|
48096
|
+
* This collapses the runner-side causes the operator can already see in logs
|
|
48097
|
+
* (`worker-lease-gone`, `no-registry-entry`, `jpeg-encode-failed`) because
|
|
48098
|
+
* `pipelineRunner.getNativeCrop` answers an ROI miss with `null` and carries no
|
|
48099
|
+
* reason on the wire — and `FrameHandle` has no `deviceId`, so the runner could
|
|
48100
|
+
* not attribute the split per camera even if it reported it. Splitting this is a
|
|
48101
|
+
* framework change (a reason on the reply); until then the honest name is the
|
|
48102
|
+
* one the caller can actually stand behind.
|
|
48103
|
+
*/
|
|
48104
|
+
var REASON_NATIVE_MISS = "native-miss";
|
|
48105
|
+
/** Queued past `staleAfterMs`; the frame it wanted is gone. */
|
|
48106
|
+
var REASON_STALE_DROP = "stale-drop";
|
|
48107
|
+
/** Shed because the camera's lane was at its queue cap. */
|
|
48108
|
+
var REASON_QUEUE_OVERFLOW = "queue-overflow";
|
|
48109
|
+
/**
|
|
48110
|
+
* The OCR step returned, with nothing in it.
|
|
48111
|
+
*
|
|
48112
|
+
* The defect this counter was built for: ~35% of plate reads came back with an
|
|
48113
|
+
* empty label and fell through the detail router's `d.label.length > 0` gate
|
|
48114
|
+
* into no branch at all — no store, no log, no row. Silence read as "this camera
|
|
48115
|
+
* never saw a plate", which is a completely different fix.
|
|
48116
|
+
*/
|
|
48117
|
+
var REASON_EMPTY_READ = "empty-read";
|
|
48118
|
+
/** A plate detail with no bbox — nothing to crop, nothing to place. */
|
|
48119
|
+
var REASON_NO_BBOX = "no-bbox";
|
|
48120
|
+
/** `isPlausiblePlateRead` refused it (junk OCR off a distant/oblique plate). */
|
|
48121
|
+
var REASON_IMPLAUSIBLE = "implausible";
|
|
48122
|
+
/** Below the camera's configured minimum OCR confidence. */
|
|
48123
|
+
var REASON_BELOW_MIN_CONFIDENCE = "below-min-confidence";
|
|
48124
|
+
/** A person born with its box inside a vehicle's. See {@link FAMILY_PERSON_BIRTH}. */
|
|
48125
|
+
var REASON_PERSON_OVER_VEHICLE = "person-over-vehicle";
|
|
48126
|
+
/**
|
|
48127
|
+
* This addon's `failure-contribution` counters.
|
|
48128
|
+
*
|
|
48129
|
+
* One instance per addon process, handed to the paths that fail. Every method
|
|
48130
|
+
* takes the numeric `deviceId` first, for the same reason every log line does:
|
|
48131
|
+
* the question is always per camera.
|
|
48132
|
+
*/
|
|
48133
|
+
var AnalyticsFailureReport = class {
|
|
48134
|
+
now;
|
|
48135
|
+
counters;
|
|
48136
|
+
constructor(now = Date.now, counters = new require_dist.FailureCounters()) {
|
|
48137
|
+
this.now = now;
|
|
48138
|
+
this.counters = counters;
|
|
48139
|
+
}
|
|
48140
|
+
/** One enrichment crop capture. `reason` absent = the native ROI landed. */
|
|
48141
|
+
noteEnrichmentCrop(deviceId, reason) {
|
|
48142
|
+
this.counters.note({
|
|
48143
|
+
deviceId,
|
|
48144
|
+
family: FAMILY_ENRICHMENT_CROP,
|
|
48145
|
+
...reason !== void 0 ? { reason } : {}
|
|
48146
|
+
}, this.now());
|
|
48147
|
+
}
|
|
48148
|
+
/** One capture that reached a terminal scheduling outcome. */
|
|
48149
|
+
noteCaptureSchedule(deviceId, reason) {
|
|
48150
|
+
this.counters.note({
|
|
48151
|
+
deviceId,
|
|
48152
|
+
family: FAMILY_CAPTURE_SCHEDULE,
|
|
48153
|
+
...reason !== void 0 ? { reason } : {}
|
|
48154
|
+
}, this.now());
|
|
48155
|
+
}
|
|
48156
|
+
/** One plate OCR read. `reason` absent = the read was accepted and stored. */
|
|
48157
|
+
notePlateRead(deviceId, reason) {
|
|
48158
|
+
this.counters.note({
|
|
48159
|
+
deviceId,
|
|
48160
|
+
family: FAMILY_PLATE_OCR,
|
|
48161
|
+
...reason !== void 0 ? { reason } : {}
|
|
48162
|
+
}, this.now());
|
|
48163
|
+
}
|
|
48164
|
+
/** One confirmed person birth. `reason` present = it was born over a vehicle. */
|
|
48165
|
+
notePersonBirth(deviceId, reason) {
|
|
48166
|
+
this.counters.note({
|
|
48167
|
+
deviceId,
|
|
48168
|
+
family: FAMILY_PERSON_BIRTH,
|
|
48169
|
+
...reason !== void 0 ? { reason } : {}
|
|
48170
|
+
}, this.now());
|
|
48171
|
+
}
|
|
48172
|
+
/** The `failure-contribution` provider's payload. Reads, never resets. */
|
|
48173
|
+
list() {
|
|
48174
|
+
return this.counters.snapshot(this.now());
|
|
48175
|
+
}
|
|
48176
|
+
/** Addon disposal. */
|
|
48177
|
+
clear() {
|
|
48178
|
+
this.counters.clear();
|
|
48179
|
+
}
|
|
48180
|
+
};
|
|
48181
|
+
//#endregion
|
|
48182
|
+
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
48183
|
+
/**
|
|
48184
|
+
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
48185
|
+
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
48186
|
+
*
|
|
48187
|
+
* Formula (before clamping):
|
|
48188
|
+
* newX = x - padding * w
|
|
48189
|
+
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
48190
|
+
* newY = y - padding * h
|
|
48191
|
+
* newH = h + 2 * padding * h
|
|
48192
|
+
*
|
|
48193
|
+
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
48194
|
+
* new object.
|
|
48195
|
+
*
|
|
48196
|
+
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
48197
|
+
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
48198
|
+
* It shrinks only when the padded window is larger than the frame itself
|
|
48199
|
+
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
48200
|
+
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
48201
|
+
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
48202
|
+
*/
|
|
48203
|
+
function padBbox(bbox, padding) {
|
|
48204
|
+
const rawX = bbox.x - padding * bbox.w;
|
|
48205
|
+
const rawY = bbox.y - padding * bbox.h;
|
|
48206
|
+
const rawW = bbox.w * (1 + 2 * padding);
|
|
48207
|
+
const rawH = bbox.h * (1 + 2 * padding);
|
|
48208
|
+
const w = Math.min(rawW, 1);
|
|
48209
|
+
const h = Math.min(rawH, 1);
|
|
48210
|
+
return {
|
|
48211
|
+
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
48212
|
+
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
48213
|
+
w,
|
|
48214
|
+
h
|
|
48215
|
+
};
|
|
48216
|
+
}
|
|
48217
|
+
//#endregion
|
|
48218
|
+
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
48219
|
+
/**
|
|
48220
|
+
* The normalised window a capture asks the runner for: the box padded by
|
|
48221
|
+
* `padding` and slid inside the unit square. ONE derivation, shared by the
|
|
48222
|
+
* capture below and by any caller that must put a crop-relative box back
|
|
48223
|
+
* into the frame (the confirmation gate's secondary subject) — two copies of
|
|
48224
|
+
* this arithmetic would drift, and a drifted one places the subject somewhere
|
|
48225
|
+
* it is not.
|
|
48226
|
+
*/
|
|
48227
|
+
function captureCropWindow(bbox, frameWidth, frameHeight, padding) {
|
|
48228
|
+
return padBbox({
|
|
48229
|
+
x: bbox.x / frameWidth,
|
|
48230
|
+
y: bbox.y / frameHeight,
|
|
48231
|
+
w: bbox.w / frameWidth,
|
|
48232
|
+
h: bbox.h / frameHeight
|
|
48233
|
+
}, padding);
|
|
48234
|
+
}
|
|
48235
|
+
/** {@link captureCropWindow} in FRAME pixels. */
|
|
48236
|
+
function captureCropFrameRect(bbox, frameWidth, frameHeight, padding) {
|
|
48237
|
+
const window = captureCropWindow(bbox, frameWidth, frameHeight, padding);
|
|
48238
|
+
return {
|
|
48239
|
+
x: window.x * frameWidth,
|
|
48240
|
+
y: window.y * frameHeight,
|
|
48241
|
+
w: window.w * frameWidth,
|
|
48242
|
+
h: window.h * frameHeight
|
|
48243
|
+
};
|
|
48244
|
+
}
|
|
48245
|
+
function createCaptureCrop(deps) {
|
|
48246
|
+
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
48247
|
+
const paddedNorm = captureCropWindow(bbox, frameWidth, frameHeight, padding);
|
|
48248
|
+
const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
48249
|
+
if (nativeCrop) {
|
|
48250
|
+
deps.bumpCropMetric(true);
|
|
48251
|
+
if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId);
|
|
48252
|
+
return nativeCrop;
|
|
48253
|
+
}
|
|
48254
|
+
deps.bumpCropMetric(false);
|
|
48255
|
+
if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId, REASON_NATIVE_MISS);
|
|
48256
|
+
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
|
|
48257
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
48258
|
+
meta: { nodeId: frameHandle.nodeId }
|
|
48259
|
+
});
|
|
48260
|
+
return null;
|
|
48261
|
+
};
|
|
48262
|
+
}
|
|
48263
|
+
//#endregion
|
|
48264
|
+
//#region src/pipeline-analytics/pipeline/secondary-subjects.ts
|
|
48265
|
+
/**
|
|
48266
|
+
* How long a promoted subject may keep riding. Rides are consumed within
|
|
48267
|
+
* ~300 ms at 10 fps; this bound is what stops a promotion made just before a
|
|
48268
|
+
* stall from riding a frame that belongs to a scene that has moved on.
|
|
48269
|
+
*/
|
|
48270
|
+
var SECONDARY_SUBJECT_TTL_MS = 3e3;
|
|
48271
|
+
var SecondarySubjectQueue = class {
|
|
48272
|
+
byKey = /* @__PURE__ */ new Map();
|
|
48273
|
+
nextId = 0;
|
|
48274
|
+
add(key, subject, foundBesideTrackId, now) {
|
|
48275
|
+
const pending = this.byKey.get(key) ?? [];
|
|
48276
|
+
this.nextId += 1;
|
|
48277
|
+
this.byKey.set(key, [...pending, {
|
|
48278
|
+
id: `s${this.nextId}`,
|
|
48279
|
+
subject,
|
|
48280
|
+
foundBesideTrackId,
|
|
48281
|
+
expiresAt: now + SECONDARY_SUBJECT_TTL_MS,
|
|
48282
|
+
ridesLeft: 3
|
|
48283
|
+
}]);
|
|
48284
|
+
}
|
|
48285
|
+
/**
|
|
48286
|
+
* Every subject still owed a ride for `key`, one ride spent. An entry out of
|
|
48287
|
+
* rides or past its TTL is forgotten here rather than served.
|
|
48288
|
+
*/
|
|
48289
|
+
take(key, now) {
|
|
48290
|
+
const pending = this.byKey.get(key);
|
|
48291
|
+
if (!pending) return [];
|
|
48292
|
+
const live = pending.filter((entry) => entry.expiresAt > now && entry.ridesLeft > 0);
|
|
48293
|
+
const remaining = live.map((entry) => ({
|
|
48294
|
+
...entry,
|
|
48295
|
+
ridesLeft: entry.ridesLeft - 1
|
|
48296
|
+
})).filter((entry) => entry.ridesLeft > 0);
|
|
48297
|
+
if (remaining.length === 0) this.byKey.delete(key);
|
|
48298
|
+
else this.byKey.set(key, remaining);
|
|
48299
|
+
return live.map((entry) => ({
|
|
48300
|
+
id: entry.id,
|
|
48301
|
+
subject: entry.subject,
|
|
48302
|
+
foundBesideTrackId: entry.foundBesideTrackId
|
|
48303
|
+
}));
|
|
48304
|
+
}
|
|
48305
|
+
/** A subject a real detection has taken over rides no further. */
|
|
48306
|
+
drop(key, id) {
|
|
48307
|
+
const pending = this.byKey.get(key);
|
|
48308
|
+
if (!pending) return;
|
|
48309
|
+
const remaining = pending.filter((entry) => entry.id !== id);
|
|
48310
|
+
if (remaining.length === 0) this.byKey.delete(key);
|
|
48311
|
+
else this.byKey.set(key, remaining);
|
|
48312
|
+
}
|
|
48313
|
+
forget(key) {
|
|
48314
|
+
this.byKey.delete(key);
|
|
48315
|
+
}
|
|
48316
|
+
};
|
|
48317
|
+
function iou$2(a, b) {
|
|
48318
|
+
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));
|
|
48319
|
+
const union = a.w * a.h + b.width * b.height - inter;
|
|
48320
|
+
return union > 0 ? inter / union : 0;
|
|
48321
|
+
}
|
|
48322
|
+
/**
|
|
48323
|
+
* The runner's detections plus one synthetic first-level detection per pending
|
|
48324
|
+
* subject that no same-class detection already covers. The synthetic box
|
|
48325
|
+
* carries the gate's score — a measurement on a native crop, not an invention
|
|
48326
|
+
* — and an id that says where it came from. The original result is not
|
|
48327
|
+
* mutated.
|
|
48328
|
+
*/
|
|
48329
|
+
function injectSecondarySubjects(frame, pending) {
|
|
48330
|
+
if (pending.length === 0) return {
|
|
48331
|
+
frame,
|
|
48332
|
+
covered: []
|
|
48333
|
+
};
|
|
48334
|
+
const added = [];
|
|
48335
|
+
const covered = [];
|
|
48336
|
+
for (const { id, subject, foundBesideTrackId } of pending) {
|
|
48337
|
+
if ([...frame.detections, ...added].some((d) => d.kind === "first-level" && d.macroClass === subject.className && iou$2(subject.bbox, d.bbox) >= .5)) {
|
|
48338
|
+
covered.push(id);
|
|
48339
|
+
continue;
|
|
48340
|
+
}
|
|
48341
|
+
added.push({
|
|
48342
|
+
id: `secondary-${id}-${foundBesideTrackId}`,
|
|
48343
|
+
kind: "first-level",
|
|
48344
|
+
macroClass: subject.className,
|
|
48345
|
+
score: subject.score,
|
|
48346
|
+
labels: [],
|
|
48347
|
+
bbox: {
|
|
48348
|
+
x: subject.bbox.x,
|
|
48349
|
+
y: subject.bbox.y,
|
|
48350
|
+
width: subject.bbox.w,
|
|
48351
|
+
height: subject.bbox.h
|
|
48352
|
+
}
|
|
48353
|
+
});
|
|
48354
|
+
}
|
|
48355
|
+
if (added.length === 0) return {
|
|
48356
|
+
frame,
|
|
48357
|
+
covered
|
|
48358
|
+
};
|
|
48359
|
+
return {
|
|
48360
|
+
frame: {
|
|
48361
|
+
...frame,
|
|
48362
|
+
detections: [...frame.detections, ...added]
|
|
48363
|
+
},
|
|
48364
|
+
covered
|
|
48365
|
+
};
|
|
48366
|
+
}
|
|
48013
48367
|
var MACRO_SET = new Set([
|
|
48014
48368
|
"person",
|
|
48015
48369
|
"vehicle",
|
|
@@ -51859,42 +52213,6 @@ function decideKeyFrameCapture(input) {
|
|
|
51859
52213
|
return { kind: "retry" };
|
|
51860
52214
|
}
|
|
51861
52215
|
//#endregion
|
|
51862
|
-
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
51863
|
-
/**
|
|
51864
|
-
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
51865
|
-
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
51866
|
-
*
|
|
51867
|
-
* Formula (before clamping):
|
|
51868
|
-
* newX = x - padding * w
|
|
51869
|
-
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
51870
|
-
* newY = y - padding * h
|
|
51871
|
-
* newH = h + 2 * padding * h
|
|
51872
|
-
*
|
|
51873
|
-
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
51874
|
-
* new object.
|
|
51875
|
-
*
|
|
51876
|
-
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
51877
|
-
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
51878
|
-
* It shrinks only when the padded window is larger than the frame itself
|
|
51879
|
-
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
51880
|
-
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
51881
|
-
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
51882
|
-
*/
|
|
51883
|
-
function padBbox(bbox, padding) {
|
|
51884
|
-
const rawX = bbox.x - padding * bbox.w;
|
|
51885
|
-
const rawY = bbox.y - padding * bbox.h;
|
|
51886
|
-
const rawW = bbox.w * (1 + 2 * padding);
|
|
51887
|
-
const rawH = bbox.h * (1 + 2 * padding);
|
|
51888
|
-
const w = Math.min(rawW, 1);
|
|
51889
|
-
const h = Math.min(rawH, 1);
|
|
51890
|
-
return {
|
|
51891
|
-
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
51892
|
-
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
51893
|
-
w,
|
|
51894
|
-
h
|
|
51895
|
-
};
|
|
51896
|
-
}
|
|
51897
|
-
//#endregion
|
|
51898
52216
|
//#region src/pipeline-analytics/pipeline/subject-agreement.ts
|
|
51899
52217
|
/**
|
|
51900
52218
|
* `subjectAgreement` — the ONE predicate for "a rectangle measured on frame A
|
|
@@ -55249,150 +55567,6 @@ function votePlateText(reads) {
|
|
|
55249
55567
|
return out;
|
|
55250
55568
|
}
|
|
55251
55569
|
//#endregion
|
|
55252
|
-
//#region src/pipeline-analytics/runtime/failure-report.ts
|
|
55253
|
-
/**
|
|
55254
|
-
* What `pipeline-analytics` reports through `failure-contribution` — the four
|
|
55255
|
-
* per-camera counters, and the ONE place their vocabulary is written down.
|
|
55256
|
-
*
|
|
55257
|
-
* ## Why the vocabulary is a module and not four string literals
|
|
55258
|
-
*
|
|
55259
|
-
* D276 gave a track row a `previewMissReason` composed from strings like
|
|
55260
|
-
* `keyframe:native-miss` and `thumbnail:overflow`. Those strings are now
|
|
55261
|
-
* joinable evidence: a row says which track lost its tile, and these counters
|
|
55262
|
-
* say how often that camera loses one and out of how many tries. A second
|
|
55263
|
-
* vocabulary for the same loss would break the join silently — the row would
|
|
55264
|
-
* say `native-miss` and the counter `nativeMiss`, and nobody would notice until
|
|
55265
|
-
* somebody tried to correlate them. So the reasons live here, named once, and
|
|
55266
|
-
* they deliberately reuse D276's spelling.
|
|
55267
|
-
*
|
|
55268
|
-
* ## Every family carries its denominator
|
|
55269
|
-
*
|
|
55270
|
-
* Each `note*` call is ONE attempt. The success calls are not decoration: a
|
|
55271
|
-
* failure count without them is the number that read as "35x worse than
|
|
55272
|
-
* yesterday" on 2026-08-28 and was flat all day once divided
|
|
55273
|
-
* (`packages/types/src/metrics/failure-counters.ts`).
|
|
55274
|
-
*
|
|
55275
|
-
* ## Nothing here logs
|
|
55276
|
-
*
|
|
55277
|
-
* These paths run at frame rate — 507 enrichment-crop misses in five minutes at
|
|
55278
|
-
* peak, ~100/min. A line each is its own defect, and the log lines that already
|
|
55279
|
-
* exist on these paths (`enrichment crop native miss`, `capture queue full`,
|
|
55280
|
-
* `plate: read discarded`) are unchanged and still throttled. This is the
|
|
55281
|
-
* counter that makes them countable without reading them.
|
|
55282
|
-
*/
|
|
55283
|
-
/**
|
|
55284
|
-
* The enrichment crop capture — face / plate / clip / detail-classifier.
|
|
55285
|
-
* Denominator: every crop this addon asked the owning runner for.
|
|
55286
|
-
*/
|
|
55287
|
-
var FAMILY_ENRICHMENT_CROP = "enrichment-crop";
|
|
55288
|
-
/**
|
|
55289
|
-
* The per-camera capture scheduler lane. Denominator: every queued capture that
|
|
55290
|
-
* reached a TERMINAL scheduling outcome — started, or dropped. A coalesced or
|
|
55291
|
-
* cancelled request is neither and is deliberately not counted: a track that
|
|
55292
|
-
* closed before its capture ran did not lose work, it stopped wanting it.
|
|
55293
|
-
*/
|
|
55294
|
-
var FAMILY_CAPTURE_SCHEDULE = "capture-schedule";
|
|
55295
|
-
/** Plate OCR reads routed from the detail plane. Denominator: every read. */
|
|
55296
|
-
var FAMILY_PLATE_OCR = "plate-ocr";
|
|
55297
|
-
/**
|
|
55298
|
-
* Person tracks born on this camera. Denominator: every confirmed person birth.
|
|
55299
|
-
*
|
|
55300
|
-
* The "failure" here is NOT suppressed and must not be — a person standing over
|
|
55301
|
-
* a car is a real person, and this repo will not trade that away for a cleaner
|
|
55302
|
-
* count (D275 made the same choice for stationary suppression). It is counted so
|
|
55303
|
-
* the phantom rate is a number instead of an impression: 4 of 23 on camera 617
|
|
55304
|
-
* in two hours, measured by hand on 2026-08-28.
|
|
55305
|
-
*/
|
|
55306
|
-
var FAMILY_PERSON_BIRTH = "person-birth";
|
|
55307
|
-
/**
|
|
55308
|
-
* The owning runner had no native pixels for the ROI.
|
|
55309
|
-
*
|
|
55310
|
-
* This collapses the runner-side causes the operator can already see in logs
|
|
55311
|
-
* (`worker-lease-gone`, `no-registry-entry`, `jpeg-encode-failed`) because
|
|
55312
|
-
* `pipelineRunner.getNativeCrop` answers an ROI miss with `null` and carries no
|
|
55313
|
-
* reason on the wire — and `FrameHandle` has no `deviceId`, so the runner could
|
|
55314
|
-
* not attribute the split per camera even if it reported it. Splitting this is a
|
|
55315
|
-
* framework change (a reason on the reply); until then the honest name is the
|
|
55316
|
-
* one the caller can actually stand behind.
|
|
55317
|
-
*/
|
|
55318
|
-
var REASON_NATIVE_MISS = "native-miss";
|
|
55319
|
-
/** Queued past `staleAfterMs`; the frame it wanted is gone. */
|
|
55320
|
-
var REASON_STALE_DROP = "stale-drop";
|
|
55321
|
-
/** Shed because the camera's lane was at its queue cap. */
|
|
55322
|
-
var REASON_QUEUE_OVERFLOW = "queue-overflow";
|
|
55323
|
-
/**
|
|
55324
|
-
* The OCR step returned, with nothing in it.
|
|
55325
|
-
*
|
|
55326
|
-
* The defect this counter was built for: ~35% of plate reads came back with an
|
|
55327
|
-
* empty label and fell through the detail router's `d.label.length > 0` gate
|
|
55328
|
-
* into no branch at all — no store, no log, no row. Silence read as "this camera
|
|
55329
|
-
* never saw a plate", which is a completely different fix.
|
|
55330
|
-
*/
|
|
55331
|
-
var REASON_EMPTY_READ = "empty-read";
|
|
55332
|
-
/** A plate detail with no bbox — nothing to crop, nothing to place. */
|
|
55333
|
-
var REASON_NO_BBOX = "no-bbox";
|
|
55334
|
-
/** `isPlausiblePlateRead` refused it (junk OCR off a distant/oblique plate). */
|
|
55335
|
-
var REASON_IMPLAUSIBLE = "implausible";
|
|
55336
|
-
/** Below the camera's configured minimum OCR confidence. */
|
|
55337
|
-
var REASON_BELOW_MIN_CONFIDENCE = "below-min-confidence";
|
|
55338
|
-
/** A person born with its box inside a vehicle's. See {@link FAMILY_PERSON_BIRTH}. */
|
|
55339
|
-
var REASON_PERSON_OVER_VEHICLE = "person-over-vehicle";
|
|
55340
|
-
/**
|
|
55341
|
-
* This addon's `failure-contribution` counters.
|
|
55342
|
-
*
|
|
55343
|
-
* One instance per addon process, handed to the paths that fail. Every method
|
|
55344
|
-
* takes the numeric `deviceId` first, for the same reason every log line does:
|
|
55345
|
-
* the question is always per camera.
|
|
55346
|
-
*/
|
|
55347
|
-
var AnalyticsFailureReport = class {
|
|
55348
|
-
now;
|
|
55349
|
-
counters;
|
|
55350
|
-
constructor(now = Date.now, counters = new require_dist.FailureCounters()) {
|
|
55351
|
-
this.now = now;
|
|
55352
|
-
this.counters = counters;
|
|
55353
|
-
}
|
|
55354
|
-
/** One enrichment crop capture. `reason` absent = the native ROI landed. */
|
|
55355
|
-
noteEnrichmentCrop(deviceId, reason) {
|
|
55356
|
-
this.counters.note({
|
|
55357
|
-
deviceId,
|
|
55358
|
-
family: FAMILY_ENRICHMENT_CROP,
|
|
55359
|
-
...reason !== void 0 ? { reason } : {}
|
|
55360
|
-
}, this.now());
|
|
55361
|
-
}
|
|
55362
|
-
/** One capture that reached a terminal scheduling outcome. */
|
|
55363
|
-
noteCaptureSchedule(deviceId, reason) {
|
|
55364
|
-
this.counters.note({
|
|
55365
|
-
deviceId,
|
|
55366
|
-
family: FAMILY_CAPTURE_SCHEDULE,
|
|
55367
|
-
...reason !== void 0 ? { reason } : {}
|
|
55368
|
-
}, this.now());
|
|
55369
|
-
}
|
|
55370
|
-
/** One plate OCR read. `reason` absent = the read was accepted and stored. */
|
|
55371
|
-
notePlateRead(deviceId, reason) {
|
|
55372
|
-
this.counters.note({
|
|
55373
|
-
deviceId,
|
|
55374
|
-
family: FAMILY_PLATE_OCR,
|
|
55375
|
-
...reason !== void 0 ? { reason } : {}
|
|
55376
|
-
}, this.now());
|
|
55377
|
-
}
|
|
55378
|
-
/** One confirmed person birth. `reason` present = it was born over a vehicle. */
|
|
55379
|
-
notePersonBirth(deviceId, reason) {
|
|
55380
|
-
this.counters.note({
|
|
55381
|
-
deviceId,
|
|
55382
|
-
family: FAMILY_PERSON_BIRTH,
|
|
55383
|
-
...reason !== void 0 ? { reason } : {}
|
|
55384
|
-
}, this.now());
|
|
55385
|
-
}
|
|
55386
|
-
/** The `failure-contribution` provider's payload. Reads, never resets. */
|
|
55387
|
-
list() {
|
|
55388
|
-
return this.counters.snapshot(this.now());
|
|
55389
|
-
}
|
|
55390
|
-
/** Addon disposal. */
|
|
55391
|
-
clear() {
|
|
55392
|
-
this.counters.clear();
|
|
55393
|
-
}
|
|
55394
|
-
};
|
|
55395
|
-
//#endregion
|
|
55396
55570
|
//#region src/pipeline-analytics/plate-recognizer.ts
|
|
55397
55571
|
/**
|
|
55398
55572
|
* PlateRecognizer — collects EVERY license-plate OCR read a vehicle track
|
|
@@ -60451,31 +60625,6 @@ var NativeCropDedupe = class {
|
|
|
60451
60625
|
}
|
|
60452
60626
|
};
|
|
60453
60627
|
//#endregion
|
|
60454
|
-
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
60455
|
-
function createCaptureCrop(deps) {
|
|
60456
|
-
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
60457
|
-
const paddedNorm = padBbox({
|
|
60458
|
-
x: bbox.x / frameWidth,
|
|
60459
|
-
y: bbox.y / frameHeight,
|
|
60460
|
-
w: bbox.w / frameWidth,
|
|
60461
|
-
h: bbox.h / frameHeight
|
|
60462
|
-
}, padding);
|
|
60463
|
-
const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
60464
|
-
if (nativeCrop) {
|
|
60465
|
-
deps.bumpCropMetric(true);
|
|
60466
|
-
if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId);
|
|
60467
|
-
return nativeCrop;
|
|
60468
|
-
}
|
|
60469
|
-
deps.bumpCropMetric(false);
|
|
60470
|
-
if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId, REASON_NATIVE_MISS);
|
|
60471
|
-
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
|
|
60472
|
-
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
60473
|
-
meta: { nodeId: frameHandle.nodeId }
|
|
60474
|
-
});
|
|
60475
|
-
return null;
|
|
60476
|
-
};
|
|
60477
|
-
}
|
|
60478
|
-
//#endregion
|
|
60479
60628
|
//#region src/pipeline-analytics/pipeline/display-crop.ts
|
|
60480
60629
|
/** Bound the once-per-owner memo so a long-lived process never leaks it. */
|
|
60481
60630
|
var MAX_LOGGED_OWNERS = 5e3;
|
|
@@ -65351,6 +65500,144 @@ function rowFromMetadata(id, metadata) {
|
|
|
65351
65500
|
};
|
|
65352
65501
|
}
|
|
65353
65502
|
//#endregion
|
|
65503
|
+
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
65504
|
+
/** How long a mirrored entry is served before a refresh is started behind the
|
|
65505
|
+
* next reader. See the module doc for why this matches the per-kind caches'
|
|
65506
|
+
* own window rather than introducing a second staleness bound. */
|
|
65507
|
+
var RAW_SETTINGS_STALE_MS = 5e3;
|
|
65508
|
+
/**
|
|
65509
|
+
* How long a FAILED refresh is not retried.
|
|
65510
|
+
*
|
|
65511
|
+
* A refresh that throws leaves `fetchedAt` where it was, so without a
|
|
65512
|
+
* cooldown every subsequent reader would start another attempt — a broken
|
|
65513
|
+
* settings store would be hammered at frame rate by the very mechanism built
|
|
65514
|
+
* to stop that.
|
|
65515
|
+
*/
|
|
65516
|
+
var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
|
|
65517
|
+
/**
|
|
65518
|
+
* The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
|
|
65519
|
+
* fresh OFF every per-kind settings cache's hot path.
|
|
65520
|
+
*
|
|
65521
|
+
* `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
|
|
65522
|
+
* (or frames) missing on the same device in the same tick costs ONE read,
|
|
65523
|
+
* not one per caller — the exact failure this class exists to remove.
|
|
65524
|
+
*/
|
|
65525
|
+
var RawDeviceBlobMirror = class {
|
|
65526
|
+
deps;
|
|
65527
|
+
staleMs;
|
|
65528
|
+
missCooldownMs;
|
|
65529
|
+
entries = /* @__PURE__ */ new Map();
|
|
65530
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
65531
|
+
now;
|
|
65532
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
65533
|
+
this.deps = deps;
|
|
65534
|
+
this.staleMs = staleMs;
|
|
65535
|
+
this.missCooldownMs = missCooldownMs;
|
|
65536
|
+
this.now = deps.now ?? (() => Date.now());
|
|
65537
|
+
}
|
|
65538
|
+
/**
|
|
65539
|
+
* The device's raw stored blob.
|
|
65540
|
+
*
|
|
65541
|
+
* COLD awaits — the first settings kind to touch a device has to get the
|
|
65542
|
+
* real answer. WARM returns immediately, stale or not, and starts the
|
|
65543
|
+
* refresh behind the caller.
|
|
65544
|
+
*/
|
|
65545
|
+
async resolve(deviceId) {
|
|
65546
|
+
const entry = this.entries.get(deviceId);
|
|
65547
|
+
if (entry === void 0) return this.load(deviceId);
|
|
65548
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
|
|
65549
|
+
return entry.value;
|
|
65550
|
+
}
|
|
65551
|
+
/** Drop a device's entry. Called by every in-process writer of this
|
|
65552
|
+
* device's row, so an operator's own change is never served stale. */
|
|
65553
|
+
invalidate(deviceId) {
|
|
65554
|
+
this.entries.delete(deviceId);
|
|
65555
|
+
}
|
|
65556
|
+
invalidateAll() {
|
|
65557
|
+
this.entries.clear();
|
|
65558
|
+
}
|
|
65559
|
+
async load(deviceId) {
|
|
65560
|
+
const pending = this.inFlight.get(deviceId);
|
|
65561
|
+
if (pending !== void 0) return pending;
|
|
65562
|
+
const run = this.deps.read(deviceId).then((value) => {
|
|
65563
|
+
this.entries.set(deviceId, {
|
|
65564
|
+
value,
|
|
65565
|
+
fetchedAt: this.now()
|
|
65566
|
+
});
|
|
65567
|
+
return value;
|
|
65568
|
+
}).catch((err) => {
|
|
65569
|
+
this.deps.onRefreshFailed?.(deviceId, err);
|
|
65570
|
+
const existing = this.entries.get(deviceId);
|
|
65571
|
+
if (existing !== void 0) {
|
|
65572
|
+
existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
|
|
65573
|
+
return existing.value;
|
|
65574
|
+
}
|
|
65575
|
+
return {};
|
|
65576
|
+
}).finally(() => {
|
|
65577
|
+
this.inFlight.delete(deviceId);
|
|
65578
|
+
});
|
|
65579
|
+
this.inFlight.set(deviceId, run);
|
|
65580
|
+
return run;
|
|
65581
|
+
}
|
|
65582
|
+
};
|
|
65583
|
+
/**
|
|
65584
|
+
* The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
|
|
65585
|
+
* kept fresh OFF every per-kind settings cache's hot path. Same shape as
|
|
65586
|
+
* {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
|
|
65587
|
+
* one addon store per process.
|
|
65588
|
+
*/
|
|
65589
|
+
var RawGlobalStoreMirror = class {
|
|
65590
|
+
deps;
|
|
65591
|
+
staleMs;
|
|
65592
|
+
missCooldownMs;
|
|
65593
|
+
entry = null;
|
|
65594
|
+
inFlight = null;
|
|
65595
|
+
now;
|
|
65596
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
65597
|
+
this.deps = deps;
|
|
65598
|
+
this.staleMs = staleMs;
|
|
65599
|
+
this.missCooldownMs = missCooldownMs;
|
|
65600
|
+
this.now = deps.now ?? (() => Date.now());
|
|
65601
|
+
}
|
|
65602
|
+
async resolve() {
|
|
65603
|
+
const entry = this.entry;
|
|
65604
|
+
if (entry === null) return this.load();
|
|
65605
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
|
|
65606
|
+
return entry.value;
|
|
65607
|
+
}
|
|
65608
|
+
/** Drop the mirrored store. Called by every in-process global-settings
|
|
65609
|
+
* writer (`onConfigChanged`), so an operator's own change is never served
|
|
65610
|
+
* stale. */
|
|
65611
|
+
invalidate() {
|
|
65612
|
+
this.entry = null;
|
|
65613
|
+
}
|
|
65614
|
+
async load() {
|
|
65615
|
+
if (this.inFlight !== null) return this.inFlight;
|
|
65616
|
+
const run = this.deps.read().then((value) => {
|
|
65617
|
+
this.entry = {
|
|
65618
|
+
value,
|
|
65619
|
+
fetchedAt: this.now()
|
|
65620
|
+
};
|
|
65621
|
+
return value;
|
|
65622
|
+
}).catch((err) => {
|
|
65623
|
+
this.deps.onRefreshFailed?.(err);
|
|
65624
|
+
const existing = this.entry;
|
|
65625
|
+
if (existing !== null) {
|
|
65626
|
+
this.entry = {
|
|
65627
|
+
value: existing.value,
|
|
65628
|
+
fetchedAt: this.now() - this.staleMs + this.missCooldownMs
|
|
65629
|
+
};
|
|
65630
|
+
return existing.value;
|
|
65631
|
+
}
|
|
65632
|
+
return {};
|
|
65633
|
+
}).finally(() => {
|
|
65634
|
+
this.inFlight = null;
|
|
65635
|
+
});
|
|
65636
|
+
this.inFlight = run;
|
|
65637
|
+
return run;
|
|
65638
|
+
}
|
|
65639
|
+
};
|
|
65640
|
+
//#endregion
|
|
65354
65641
|
//#region src/pipeline-analytics/summary/summary-score.ts
|
|
65355
65642
|
/** A member whose own box is covered this much by others is hidden. */
|
|
65356
65643
|
var OCCLUDED_COVER_FRACTION = .5;
|
|
@@ -65655,144 +65942,6 @@ var SummarySealer = class {
|
|
|
65655
65942
|
}
|
|
65656
65943
|
};
|
|
65657
65944
|
//#endregion
|
|
65658
|
-
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
65659
|
-
/** How long a mirrored entry is served before a refresh is started behind the
|
|
65660
|
-
* next reader. See the module doc for why this matches the per-kind caches'
|
|
65661
|
-
* own window rather than introducing a second staleness bound. */
|
|
65662
|
-
var RAW_SETTINGS_STALE_MS = 5e3;
|
|
65663
|
-
/**
|
|
65664
|
-
* How long a FAILED refresh is not retried.
|
|
65665
|
-
*
|
|
65666
|
-
* A refresh that throws leaves `fetchedAt` where it was, so without a
|
|
65667
|
-
* cooldown every subsequent reader would start another attempt — a broken
|
|
65668
|
-
* settings store would be hammered at frame rate by the very mechanism built
|
|
65669
|
-
* to stop that.
|
|
65670
|
-
*/
|
|
65671
|
-
var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
|
|
65672
|
-
/**
|
|
65673
|
-
* The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
|
|
65674
|
-
* fresh OFF every per-kind settings cache's hot path.
|
|
65675
|
-
*
|
|
65676
|
-
* `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
|
|
65677
|
-
* (or frames) missing on the same device in the same tick costs ONE read,
|
|
65678
|
-
* not one per caller — the exact failure this class exists to remove.
|
|
65679
|
-
*/
|
|
65680
|
-
var RawDeviceBlobMirror = class {
|
|
65681
|
-
deps;
|
|
65682
|
-
staleMs;
|
|
65683
|
-
missCooldownMs;
|
|
65684
|
-
entries = /* @__PURE__ */ new Map();
|
|
65685
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
65686
|
-
now;
|
|
65687
|
-
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
65688
|
-
this.deps = deps;
|
|
65689
|
-
this.staleMs = staleMs;
|
|
65690
|
-
this.missCooldownMs = missCooldownMs;
|
|
65691
|
-
this.now = deps.now ?? (() => Date.now());
|
|
65692
|
-
}
|
|
65693
|
-
/**
|
|
65694
|
-
* The device's raw stored blob.
|
|
65695
|
-
*
|
|
65696
|
-
* COLD awaits — the first settings kind to touch a device has to get the
|
|
65697
|
-
* real answer. WARM returns immediately, stale or not, and starts the
|
|
65698
|
-
* refresh behind the caller.
|
|
65699
|
-
*/
|
|
65700
|
-
async resolve(deviceId) {
|
|
65701
|
-
const entry = this.entries.get(deviceId);
|
|
65702
|
-
if (entry === void 0) return this.load(deviceId);
|
|
65703
|
-
if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
|
|
65704
|
-
return entry.value;
|
|
65705
|
-
}
|
|
65706
|
-
/** Drop a device's entry. Called by every in-process writer of this
|
|
65707
|
-
* device's row, so an operator's own change is never served stale. */
|
|
65708
|
-
invalidate(deviceId) {
|
|
65709
|
-
this.entries.delete(deviceId);
|
|
65710
|
-
}
|
|
65711
|
-
invalidateAll() {
|
|
65712
|
-
this.entries.clear();
|
|
65713
|
-
}
|
|
65714
|
-
async load(deviceId) {
|
|
65715
|
-
const pending = this.inFlight.get(deviceId);
|
|
65716
|
-
if (pending !== void 0) return pending;
|
|
65717
|
-
const run = this.deps.read(deviceId).then((value) => {
|
|
65718
|
-
this.entries.set(deviceId, {
|
|
65719
|
-
value,
|
|
65720
|
-
fetchedAt: this.now()
|
|
65721
|
-
});
|
|
65722
|
-
return value;
|
|
65723
|
-
}).catch((err) => {
|
|
65724
|
-
this.deps.onRefreshFailed?.(deviceId, err);
|
|
65725
|
-
const existing = this.entries.get(deviceId);
|
|
65726
|
-
if (existing !== void 0) {
|
|
65727
|
-
existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
|
|
65728
|
-
return existing.value;
|
|
65729
|
-
}
|
|
65730
|
-
return {};
|
|
65731
|
-
}).finally(() => {
|
|
65732
|
-
this.inFlight.delete(deviceId);
|
|
65733
|
-
});
|
|
65734
|
-
this.inFlight.set(deviceId, run);
|
|
65735
|
-
return run;
|
|
65736
|
-
}
|
|
65737
|
-
};
|
|
65738
|
-
/**
|
|
65739
|
-
* The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
|
|
65740
|
-
* kept fresh OFF every per-kind settings cache's hot path. Same shape as
|
|
65741
|
-
* {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
|
|
65742
|
-
* one addon store per process.
|
|
65743
|
-
*/
|
|
65744
|
-
var RawGlobalStoreMirror = class {
|
|
65745
|
-
deps;
|
|
65746
|
-
staleMs;
|
|
65747
|
-
missCooldownMs;
|
|
65748
|
-
entry = null;
|
|
65749
|
-
inFlight = null;
|
|
65750
|
-
now;
|
|
65751
|
-
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
65752
|
-
this.deps = deps;
|
|
65753
|
-
this.staleMs = staleMs;
|
|
65754
|
-
this.missCooldownMs = missCooldownMs;
|
|
65755
|
-
this.now = deps.now ?? (() => Date.now());
|
|
65756
|
-
}
|
|
65757
|
-
async resolve() {
|
|
65758
|
-
const entry = this.entry;
|
|
65759
|
-
if (entry === null) return this.load();
|
|
65760
|
-
if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
|
|
65761
|
-
return entry.value;
|
|
65762
|
-
}
|
|
65763
|
-
/** Drop the mirrored store. Called by every in-process global-settings
|
|
65764
|
-
* writer (`onConfigChanged`), so an operator's own change is never served
|
|
65765
|
-
* stale. */
|
|
65766
|
-
invalidate() {
|
|
65767
|
-
this.entry = null;
|
|
65768
|
-
}
|
|
65769
|
-
async load() {
|
|
65770
|
-
if (this.inFlight !== null) return this.inFlight;
|
|
65771
|
-
const run = this.deps.read().then((value) => {
|
|
65772
|
-
this.entry = {
|
|
65773
|
-
value,
|
|
65774
|
-
fetchedAt: this.now()
|
|
65775
|
-
};
|
|
65776
|
-
return value;
|
|
65777
|
-
}).catch((err) => {
|
|
65778
|
-
this.deps.onRefreshFailed?.(err);
|
|
65779
|
-
const existing = this.entry;
|
|
65780
|
-
if (existing !== null) {
|
|
65781
|
-
this.entry = {
|
|
65782
|
-
value: existing.value,
|
|
65783
|
-
fetchedAt: this.now() - this.staleMs + this.missCooldownMs
|
|
65784
|
-
};
|
|
65785
|
-
return existing.value;
|
|
65786
|
-
}
|
|
65787
|
-
return {};
|
|
65788
|
-
}).finally(() => {
|
|
65789
|
-
this.inFlight = null;
|
|
65790
|
-
});
|
|
65791
|
-
this.inFlight = run;
|
|
65792
|
-
return run;
|
|
65793
|
-
}
|
|
65794
|
-
};
|
|
65795
|
-
//#endregion
|
|
65796
65945
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
65797
65946
|
/**
|
|
65798
65947
|
* Sweep every device with persisted tracks: skip retention-disabled devices,
|
|
@@ -67380,6 +67529,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
67380
67529
|
suppressedBirths = new SuppressedBirthRegistry();
|
|
67381
67530
|
/** Births the gate could not MEASURE, awaiting another look on a later frame. */
|
|
67382
67531
|
deferredBirths = new DeferredBirthRegistry();
|
|
67532
|
+
/**
|
|
67533
|
+
* Subjects the confirmation gate found in a birth's crop BESIDES the birth
|
|
67534
|
+
* itself (a dog in a person's crop), waiting to ride the next frame of the
|
|
67535
|
+
* same (device, source) as detections — `pipeline/secondary-subjects.ts`.
|
|
67536
|
+
*/
|
|
67537
|
+
secondarySubjects = new SecondarySubjectQueue();
|
|
67383
67538
|
lastFrameDimsByDevice = /* @__PURE__ */ new Map();
|
|
67384
67539
|
lastAudioInsertByDevice = /* @__PURE__ */ new Map();
|
|
67385
67540
|
lastMotionInsertByDevice = /* @__PURE__ */ new Map();
|
|
@@ -70476,9 +70631,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70476
70631
|
processor.setZones(liveZones);
|
|
70477
70632
|
processor.setDetectionRules(liveRules);
|
|
70478
70633
|
processor.setZoneMembershipMinOverlap(trk.zoneMembershipMinOverlap);
|
|
70634
|
+
const injection = injectSecondarySubjects(frame, this.secondarySubjects.take(key, Date.now()));
|
|
70635
|
+
for (const id of injection.covered) this.secondarySubjects.drop(key, id);
|
|
70636
|
+
const trackerFrame = injection.frame;
|
|
70637
|
+
if (trackerFrame !== frame) this.ctx.logger.info("secondary subjects ride this frame as detections", {
|
|
70638
|
+
tags: { deviceId },
|
|
70639
|
+
meta: {
|
|
70640
|
+
source,
|
|
70641
|
+
added: trackerFrame.detections.length - frame.detections.length,
|
|
70642
|
+
classes: trackerFrame.detections.slice(frame.detections.length).map((d) => d.macroClass)
|
|
70643
|
+
}
|
|
70644
|
+
});
|
|
70479
70645
|
const result = processor.process({
|
|
70480
70646
|
timestamp: frame.timestamp,
|
|
70481
|
-
frame
|
|
70647
|
+
frame: trackerFrame
|
|
70482
70648
|
});
|
|
70483
70649
|
for (const r of processor.getLastLabelWriteRefusals()) {
|
|
70484
70650
|
const message = r.reason.kind === "macro-class" ? "label tier collapse refused" : "label write refused";
|
|
@@ -70621,6 +70787,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70621
70787
|
bbox: t.bbox
|
|
70622
70788
|
})), {
|
|
70623
70789
|
deviceId,
|
|
70790
|
+
source,
|
|
70624
70791
|
frameHandle,
|
|
70625
70792
|
frameWidth: result.frameWidth,
|
|
70626
70793
|
frameHeight: result.frameHeight
|
|
@@ -71265,16 +71432,25 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71265
71432
|
const executor = this.pipelineExecutorApi();
|
|
71266
71433
|
if (!executor?.runPipeline) return null;
|
|
71267
71434
|
try {
|
|
71268
|
-
const
|
|
71435
|
+
const result = await executor.runPipeline.mutate({
|
|
71269
71436
|
steps: [step],
|
|
71270
71437
|
image: new Uint8Array(cropJpeg),
|
|
71271
71438
|
deviceId,
|
|
71272
71439
|
plane: "frame"
|
|
71273
|
-
}, require_dist.nodePin(nodeId))
|
|
71440
|
+
}, require_dist.nodePin(nodeId));
|
|
71441
|
+
const detections = result?.detections;
|
|
71274
71442
|
if (!detections) return null;
|
|
71443
|
+
const cropWidth = result.width ?? 0;
|
|
71444
|
+
const cropHeight = result.height ?? 0;
|
|
71275
71445
|
return detections.map((d) => ({
|
|
71276
71446
|
macroClass: d.macroClass,
|
|
71277
|
-
score: d.score
|
|
71447
|
+
score: d.score,
|
|
71448
|
+
...d.bbox !== void 0 && cropWidth > 0 && cropHeight > 0 ? { bbox: {
|
|
71449
|
+
x: d.bbox.x / cropWidth,
|
|
71450
|
+
y: d.bbox.y / cropHeight,
|
|
71451
|
+
w: d.bbox.width / cropWidth,
|
|
71452
|
+
h: d.bbox.height / cropHeight
|
|
71453
|
+
} } : {}
|
|
71278
71454
|
}));
|
|
71279
71455
|
} catch (err) {
|
|
71280
71456
|
this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
|
|
@@ -71300,7 +71476,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71300
71476
|
if (candidates.length === 0) return allConfirmed();
|
|
71301
71477
|
const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
|
|
71302
71478
|
if (!config.enabled) return allConfirmed();
|
|
71303
|
-
const { frameHandle, frameWidth, frameHeight, deviceId } = params;
|
|
71479
|
+
const { frameHandle, frameWidth, frameHeight, deviceId, source } = params;
|
|
71304
71480
|
const captureCrop = this.captureCrop;
|
|
71305
71481
|
if (!frameHandle || !captureCrop || frameWidth <= 0 || frameHeight <= 0) {
|
|
71306
71482
|
this.ctx.logger.info("confirmation gate: no crop path — births allowed unmeasured", {
|
|
@@ -71340,6 +71516,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71340
71516
|
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, 320);
|
|
71341
71517
|
},
|
|
71342
71518
|
redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
|
|
71519
|
+
cropRectFor: (candidate) => captureCropFrameRect({
|
|
71520
|
+
x: candidate.bbox.x,
|
|
71521
|
+
y: candidate.bbox.y,
|
|
71522
|
+
w: candidate.bbox.w,
|
|
71523
|
+
h: candidate.bbox.h
|
|
71524
|
+
}, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING),
|
|
71343
71525
|
minConfidenceFor: (candidate) => this.phantomCells?.spawnBar({
|
|
71344
71526
|
deviceId,
|
|
71345
71527
|
className: candidate.className,
|
|
@@ -71384,6 +71566,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71384
71566
|
});
|
|
71385
71567
|
break;
|
|
71386
71568
|
}
|
|
71569
|
+
if (decision.secondary !== void 0) {
|
|
71570
|
+
this.secondarySubjects.add(this.procKey(deviceId, source), decision.secondary, decision.trackId, Date.now());
|
|
71571
|
+
this.ctx.logger.info("confirmation gate: secondary subject promoted from the crop", {
|
|
71572
|
+
tags: { deviceId },
|
|
71573
|
+
meta: {
|
|
71574
|
+
trackId: decision.trackId,
|
|
71575
|
+
className: decision.className,
|
|
71576
|
+
secondaryClass: decision.secondary.className,
|
|
71577
|
+
secondaryScore: decision.secondary.score,
|
|
71578
|
+
secondaryBbox: decision.secondary.bbox,
|
|
71579
|
+
verdict: decision.verdict
|
|
71580
|
+
}
|
|
71581
|
+
});
|
|
71582
|
+
}
|
|
71387
71583
|
}
|
|
71388
71584
|
}, exhaustedIds);
|
|
71389
71585
|
}
|
|
@@ -73260,6 +73456,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73260
73456
|
const prefix = `${deviceId}:`;
|
|
73261
73457
|
for (const k of this.processors.keys()) if (k.startsWith(prefix)) this.processors.delete(k);
|
|
73262
73458
|
for (const k of this.lastActiveTrackIds.keys()) if (k.startsWith(prefix)) this.lastActiveTrackIds.delete(k);
|
|
73459
|
+
for (const source of ["pipeline", "onboard"]) this.secondarySubjects.forget(this.procKey(deviceId, source));
|
|
73263
73460
|
for (const k of this.dropoutSkipsByKey.keys()) if (k.startsWith(prefix)) this.dropoutSkipsByKey.delete(k);
|
|
73264
73461
|
this.lastFrameDimsByDevice.delete(deviceId);
|
|
73265
73462
|
}
|