@camstack/addon-post-analysis 1.2.242 → 1.2.244
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/{dist-CCw4SKRp.mjs → dist-C-1xTyB-.mjs} +39 -24
- package/dist/{dist-DjW7k4lv.js → dist-DxZAf0Hs.js} +38 -23
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BBKcSFzP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DqVUwKib.mjs} +2 -2
- package/dist/pipeline-analytics/{hostInit-Blns_5Rn.mjs → hostInit-Bn-3U_Qz.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +239 -134
- package/dist/pipeline-analytics/index.mjs +239 -134
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DxZAf0Hs.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -55625,7 +55625,8 @@ function buildOverlayDetections(input) {
|
|
|
55625
55625
|
...det,
|
|
55626
55626
|
track: {
|
|
55627
55627
|
trackId: t.trackId,
|
|
55628
|
-
zones: t.zones
|
|
55628
|
+
zones: t.zones,
|
|
55629
|
+
...t.state !== void 0 ? { movementState: t.state } : {}
|
|
55629
55630
|
},
|
|
55630
55631
|
labels: hasName && !alreadyLabeled ? [...det.labels, {
|
|
55631
55632
|
label: name,
|
|
@@ -57583,6 +57584,10 @@ function centroidInside(b, p) {
|
|
|
57583
57584
|
}
|
|
57584
57585
|
/** Parked-box coverage (intersection / entry area) that counts as swallowed. */
|
|
57585
57586
|
var SWALLOW_MIN_COVERAGE = .5;
|
|
57587
|
+
/** Fraction of a detection's OWN area that must lie inside a parked entry for
|
|
57588
|
+
* the entry to swallow it. Deliberately near-total: a fragment of a parked
|
|
57589
|
+
* object does not stick out of it. */
|
|
57590
|
+
var FRAGMENT_MIN_CONTAINMENT = .9;
|
|
57586
57591
|
function isSceneSizedBox(bbox, frameWidth, frameHeight) {
|
|
57587
57592
|
const frame = frameWidth * frameHeight;
|
|
57588
57593
|
return frame > 0 && bbox.w * bbox.h / frame >= .55;
|
|
@@ -57602,6 +57607,29 @@ function detectionSwallowsEntry(det, parked) {
|
|
|
57602
57607
|
if (!centroidInside(det, centroid(parked))) return false;
|
|
57603
57608
|
return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
|
|
57604
57609
|
}
|
|
57610
|
+
/**
|
|
57611
|
+
* True when `det` is a FRAGMENT of the parked object — much smaller than the
|
|
57612
|
+
* entry and sitting (almost) wholly inside it.
|
|
57613
|
+
*
|
|
57614
|
+
* The mirror of {@link detectionSwallowsEntry}, and the case neither IoU
|
|
57615
|
+
* threshold can see. Measured on 617: a 34x137 box inside a 452x420 parked
|
|
57616
|
+
* motorcycle scores IoU 0.024 — BELOW `wakeAssocIou` (0.1), so the filter read
|
|
57617
|
+
* it as "no detection overlaps this entry" and let it open a track, accumulate
|
|
57618
|
+
* its own evidence and promote into a registry entry of its own. The camera
|
|
57619
|
+
* then reported two parked vehicles where there was one motorcycle.
|
|
57620
|
+
*
|
|
57621
|
+
* The size bound is the reciprocal of {@link SWALLOW_AREA_RATIO}, so the two
|
|
57622
|
+
* swallow rules meet without overlapping: a box between 1/1.35 and 1.35 of the
|
|
57623
|
+
* entry's area, contained, already scores IoU >= 0.74 and is answered by
|
|
57624
|
+
* `suppressIou`. Nothing is a fragment and a near-identical box at once.
|
|
57625
|
+
*/
|
|
57626
|
+
function entrySwallowsDetection(parked, det) {
|
|
57627
|
+
const entryArea = parked.w * parked.h;
|
|
57628
|
+
const detArea = det.w * det.h;
|
|
57629
|
+
if (entryArea <= 0 || detArea <= 0) return false;
|
|
57630
|
+
if (detArea > entryArea / 1.35) return false;
|
|
57631
|
+
return intersectionArea$2(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
|
|
57632
|
+
}
|
|
57605
57633
|
function parkedEntryHoldsDetection(entry, detBbox, config) {
|
|
57606
57634
|
const overlap = iou(entry.bbox, detBbox);
|
|
57607
57635
|
if (overlap >= config.suppressIou) return {
|
|
@@ -57616,6 +57644,10 @@ function parkedEntryHoldsDetection(entry, detBbox, config) {
|
|
|
57616
57644
|
strong: false,
|
|
57617
57645
|
holds: false
|
|
57618
57646
|
};
|
|
57647
|
+
if (entrySwallowsDetection(entry.bbox, detBbox)) return {
|
|
57648
|
+
strong: false,
|
|
57649
|
+
holds: true
|
|
57650
|
+
};
|
|
57619
57651
|
return {
|
|
57620
57652
|
strong: false,
|
|
57621
57653
|
holds: overlap >= config.wakeAssocIou && centroidInside(entry.bbox, centroid(detBbox))
|
|
@@ -57634,6 +57666,34 @@ function bboxMatchesParkedEntry(bbox, className, entries, suppressIou = SUPPRESS
|
|
|
57634
57666
|
}
|
|
57635
57667
|
return false;
|
|
57636
57668
|
}
|
|
57669
|
+
/**
|
|
57670
|
+
* The live entry already censusing this spot, if any — the ONE answer to "is
|
|
57671
|
+
* this place taken?", asked at BIRTH.
|
|
57672
|
+
*
|
|
57673
|
+
* It is deliberately the same predicate the per-frame filter uses
|
|
57674
|
+
* ({@link parkedEntryHoldsDetection}): a box the filter would suppress as a
|
|
57675
|
+
* parked object's own must not be able to become a second parked object. Device
|
|
57676
|
+
* 617 held four rows for one car and a fifth for a fragment of a motorcycle
|
|
57677
|
+
* because the birth path asked nothing at all.
|
|
57678
|
+
*/
|
|
57679
|
+
function findParkedOccupant(entries, className, bbox, suppressIou = SUPPRESS_IOU, wakeAssocIou = WAKE_ASSOC_IOU) {
|
|
57680
|
+
const cls = normaliseStationaryClass(className);
|
|
57681
|
+
const config = {
|
|
57682
|
+
suppressIou,
|
|
57683
|
+
wakeAssocIou
|
|
57684
|
+
};
|
|
57685
|
+
let best;
|
|
57686
|
+
for (const e of entries) {
|
|
57687
|
+
if (normaliseStationaryClass(e.className) !== cls) continue;
|
|
57688
|
+
if (!parkedEntryHoldsDetection(e, bbox, config).holds) continue;
|
|
57689
|
+
const overlap = iou(e.bbox, bbox);
|
|
57690
|
+
if (best === void 0 || overlap > best.overlap) best = {
|
|
57691
|
+
entry: e,
|
|
57692
|
+
overlap
|
|
57693
|
+
};
|
|
57694
|
+
}
|
|
57695
|
+
return best?.entry;
|
|
57696
|
+
}
|
|
57637
57697
|
/** Best-shot veto: current crop is a parked object the trail did not start on. */
|
|
57638
57698
|
function thumbnailWouldShowParkedNotSubject(input) {
|
|
57639
57699
|
if (!bboxMatchesParkedEntry(input.currentBbox, input.className, input.entries)) return false;
|
|
@@ -58619,85 +58679,6 @@ var StartedBirthRegistry = class {
|
|
|
58619
58679
|
//#endregion
|
|
58620
58680
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
|
|
58621
58681
|
/**
|
|
58622
|
-
* stationary-evidence — how a parked object EARNS a registry entry.
|
|
58623
|
-
*
|
|
58624
|
-
* ## The criterion this replaces, and why it was measuring the wrong thing
|
|
58625
|
-
*
|
|
58626
|
-
* Until 2026-08-28 an object became stationary by TRACK CONTINUITY: one track
|
|
58627
|
-
* had to stay alive and barely move for `PROMOTION_WINDOW_MS` (30 s). That
|
|
58628
|
-
* rewards an object that is *seen well and often* — precisely the opposite of
|
|
58629
|
-
* what the operator needs. Measured on device 617 (the parking lot), 24 h:
|
|
58630
|
-
*
|
|
58631
|
-
* - three cars are parked in view; the registry held exactly ONE entry;
|
|
58632
|
-
* - the two it missed were re-discovered as **140 and 112 separate tracks**,
|
|
58633
|
-
* median life **6.8 s**, p90 29 s. Not one of them could ever reach 30 s.
|
|
58634
|
-
* - those two objects alone produced **13 494 of 18 402** vehicle
|
|
58635
|
-
* observations (73%) — the whole re-spawn flood the registry exists to stop.
|
|
58636
|
-
*
|
|
58637
|
-
* The track dies; the OBJECT does not. So the evidence must outlive the track.
|
|
58638
|
-
*
|
|
58639
|
-
* ## The new criterion: a RATIO, not a count
|
|
58640
|
-
*
|
|
58641
|
-
* "The same thing, in the same place, most of the times it was seen."
|
|
58642
|
-
*
|
|
58643
|
-
* N — observations that AGREED with the candidate's anchor box (IoU ≥
|
|
58644
|
-
* {@link StationaryEvidencePolicy.agreeIou}).
|
|
58645
|
-
* M — observations OFFERED by a track once it bound to this candidate,
|
|
58646
|
-
* agreeing or not.
|
|
58647
|
-
*
|
|
58648
|
-
* A count would rescue nothing: a badly-lit car at night is seen rarely, and
|
|
58649
|
-
* any floor high enough to exclude a passer-by would exclude it too. A RATIO
|
|
58650
|
-
* asks a question a rarely-seen object can still answer well — 8 sightings out
|
|
58651
|
-
* of 10 in the same spot is stationary even though 10 is a small number.
|
|
58652
|
-
*
|
|
58653
|
-
* The dilution is the whole point, and it is why M counts a bound track's
|
|
58654
|
-
* observations even after it walks away: a car that drives THROUGH a spot binds
|
|
58655
|
-
* for a few frames and then contributes dozens of disagreeing ones. Measured on
|
|
58656
|
-
* 617 the two populations do not overlap:
|
|
58657
|
-
*
|
|
58658
|
-
* | candidate | N / M | ratio |
|
|
58659
|
-
* | ---------------------------- | ----------- | ----- |
|
|
58660
|
-
* | parked car A (missed today) | 6405 / 6467 | 0.99 |
|
|
58661
|
-
* | parked car B (missed today) | 343 / 343 | 1.00 |
|
|
58662
|
-
* | car crossing the lot | 5 / 70 | 0.07 |
|
|
58663
|
-
* | car turning through the lot | 14 / 91 | 0.15 |
|
|
58664
|
-
* | car manoeuvring | 12 / 41 | 0.29 |
|
|
58665
|
-
*
|
|
58666
|
-
* The threshold sits in the empty band between 0.29 and 0.99.
|
|
58667
|
-
*
|
|
58668
|
-
* ## The span is what a ratio cannot say
|
|
58669
|
-
*
|
|
58670
|
-
* A ratio alone promotes a car that paused for four seconds — its handful of
|
|
58671
|
-
* observations all agree. So evidence must also STRETCH: the agreeing
|
|
58672
|
-
* observations must span {@link StationaryEvidencePolicy.minSpanMs}. Measured
|
|
58673
|
-
* on 617, real parked cars span 2 min to 24 h; the two momentary stops in the
|
|
58674
|
-
* same day spanned 4.2 s and 8.0 s. Setting the span to
|
|
58675
|
-
* `DEFAULT_MAX_SESSION_HOLD_MS` also buys a structural guarantee: no single
|
|
58676
|
-
* motion session, however long it is held open, can promote on its own.
|
|
58677
|
-
*
|
|
58678
|
-
* ## ONE ledger, two producers
|
|
58679
|
-
*
|
|
58680
|
-
* Evidence is anchored to a BOX, not to a track and not to a session, so an
|
|
58681
|
-
* object confirmed half by an active motion session and half by a periodic
|
|
58682
|
-
* burst still promotes. Occasion A (the session) is implemented and is
|
|
58683
|
-
* sufficient on its own — it is free, because the pipeline is already decoding
|
|
58684
|
-
* and already inferring. Occasion B (the burst) is a precaution for the cases
|
|
58685
|
-
* where A could not have fired at all (hub restarted, camera offline, motion
|
|
58686
|
-
* detection failed, object already parked at boot); it is NOT implemented yet,
|
|
58687
|
-
* and {@link StationaryEvidenceSource} is the seam it will arrive through.
|
|
58688
|
-
*
|
|
58689
|
-
* ## Identity is IoU. There is no grid.
|
|
58690
|
-
*
|
|
58691
|
-
* The predecessor mechanism (`phantom-cells.ts`) quantises a point to a 32 px
|
|
58692
|
-
* cell, and its write path passes a CENTROID while its read path passes a
|
|
58693
|
-
* top-left CORNER — at 32 px every box bigger than 32 px lands in a different
|
|
58694
|
-
* cell on read, and its unit test never crosses that boundary because it calls
|
|
58695
|
-
* both halves with the same literal coordinates. This module has no such
|
|
58696
|
-
* boundary to get wrong: a candidate is found by overlap against the boxes a
|
|
58697
|
-
* device already holds, bounded by {@link EVIDENCE_MAX_CANDIDATES_PER_DEVICE}.
|
|
58698
|
-
* Device 617 — the busiest lot in the fleet — holds 8.
|
|
58699
|
-
*/
|
|
58700
|
-
/**
|
|
58701
58682
|
* `vehicle` and its kin. Every number below is read off device 617 over 24 h
|
|
58702
58683
|
* (18 402 vehicle observations, 290 tracks) with the three parked cars the
|
|
58703
58684
|
* operator can see out of the window as the bench.
|
|
@@ -58719,11 +58700,11 @@ var VEHICLE_EVIDENCE_POLICY = {
|
|
|
58719
58700
|
agreeIou: .5,
|
|
58720
58701
|
minAgreed: 8,
|
|
58721
58702
|
minAgreementRatio: .6,
|
|
58722
|
-
minSpanMs: 12e4
|
|
58723
|
-
promotesByDefault: true
|
|
58703
|
+
minSpanMs: 12e4
|
|
58724
58704
|
};
|
|
58725
58705
|
/**
|
|
58726
|
-
* `person`, and
|
|
58706
|
+
* `person`, promoted like every other class since 2026-09-14 — and still the
|
|
58707
|
+
* strictest numbers in the file, because these are MEASURED, not cautious.
|
|
58727
58708
|
*
|
|
58728
58709
|
* This is not caution, it is a measurement. Replaying 24 h of person
|
|
58729
58710
|
* observations from devices 617, 1443, 590 and 592 through this exact ledger:
|
|
@@ -58742,17 +58723,24 @@ var VEHICLE_EVIDENCE_POLICY = {
|
|
|
58742
58723
|
*
|
|
58743
58724
|
* Promoting a person is a BLINDING decision (D49 read in the direction that
|
|
58744
58725
|
* destroys work: for a car the destructive read is the RETIREMENT — the flood
|
|
58745
|
-
* comes back; for a person it is the BIRTH — we stop seeing them).
|
|
58746
|
-
*
|
|
58747
|
-
*
|
|
58748
|
-
*
|
|
58726
|
+
* comes back; for a person it is the BIRTH — we stop seeing them). It used to
|
|
58727
|
+
* ship OFF behind a per-camera switch for that reason; the switch is gone —
|
|
58728
|
+
* an opt-in nobody turns on produces nothing, and it was producing nothing.
|
|
58729
|
+
*
|
|
58730
|
+
* What makes that affordable now is that agreement is no longer overlap alone:
|
|
58731
|
+
* `observationAgrees` requires the TRACKER to call the subject still, so a
|
|
58732
|
+
* person who merely pauses inside the anchor no longer feeds the candidate.
|
|
58733
|
+
* The statue and the poster still promote — which is what you want, they ARE
|
|
58734
|
+
* furniture — while the real person who stops for a moment no longer does.
|
|
58735
|
+
*
|
|
58736
|
+
* The thresholds stay where the measurement put them. They are the difference
|
|
58737
|
+
* between 22 promotions a day and 10.
|
|
58749
58738
|
*/
|
|
58750
58739
|
var PERSON_EVIDENCE_POLICY = {
|
|
58751
58740
|
agreeIou: .5,
|
|
58752
58741
|
minAgreed: 40,
|
|
58753
58742
|
minAgreementRatio: .95,
|
|
58754
|
-
minSpanMs: 18e5
|
|
58755
|
-
promotesByDefault: false
|
|
58743
|
+
minSpanMs: 18e5
|
|
58756
58744
|
};
|
|
58757
58745
|
/** Classes with no policy of their own fall back to the vehicle numbers —
|
|
58758
58746
|
* `animal`, `package` and friends are all "an object that ended up somewhere",
|
|
@@ -58771,7 +58759,7 @@ function stationaryEvidenceKey(deviceId, className, anchorId) {
|
|
|
58771
58759
|
* Pure, and ordered cheapest-question-first so the reason a candidate was
|
|
58772
58760
|
* refused is the FIRST thing it failed, not the last.
|
|
58773
58761
|
*/
|
|
58774
|
-
function evaluateStationaryEvidence(row, policy
|
|
58762
|
+
function evaluateStationaryEvidence(row, policy) {
|
|
58775
58763
|
const windowSeen = row.windowSeen + row.prevWindowSeen;
|
|
58776
58764
|
const windowAgreed = row.windowAgreed + row.prevWindowAgreed;
|
|
58777
58765
|
const ratio = windowSeen > 0 ? windowAgreed / windowSeen : 0;
|
|
@@ -58782,11 +58770,6 @@ function evaluateStationaryEvidence(row, policy, classEnabled) {
|
|
|
58782
58770
|
ratio,
|
|
58783
58771
|
spanMs
|
|
58784
58772
|
};
|
|
58785
|
-
if (!classEnabled) return {
|
|
58786
|
-
kind: "accumulating",
|
|
58787
|
-
shortfall: "class-not-promoted",
|
|
58788
|
-
...base
|
|
58789
|
-
};
|
|
58790
58773
|
if (row.agreed < policy.minAgreed) return {
|
|
58791
58774
|
kind: "accumulating",
|
|
58792
58775
|
shortfall: "too-few-agreements",
|
|
@@ -58861,6 +58844,32 @@ function matchEvidenceCandidate(rows, className, bbox, agreeIou) {
|
|
|
58861
58844
|
};
|
|
58862
58845
|
}
|
|
58863
58846
|
/**
|
|
58847
|
+
* Does this observation count as EVIDENCE that the object is parked?
|
|
58848
|
+
*
|
|
58849
|
+
* Two questions, deliberately kept apart:
|
|
58850
|
+
*
|
|
58851
|
+
* - IDENTITY — is this the same object in the same place? That is the IoU,
|
|
58852
|
+
* and it is the only notion of sameness in this module.
|
|
58853
|
+
* - STILLNESS — is it going anywhere? That is the TRACKER's verdict, and
|
|
58854
|
+
* nothing here re-derives it.
|
|
58855
|
+
*
|
|
58856
|
+
* Before this, `agrees` was the IoU alone, so a long run of overlaps WAS the
|
|
58857
|
+
* stillness argument — a second derivation of a fact `StateAnalyzer` already
|
|
58858
|
+
* computes per track, slower and able to disagree with it. Now the ledger adds
|
|
58859
|
+
* only what the analyzer cannot know: that the same still object has been here
|
|
58860
|
+
* across a succession of tracks.
|
|
58861
|
+
*
|
|
58862
|
+
* `entering` is the tracker's undecided window — five frames that begin EVERY
|
|
58863
|
+
* track, including a parked car's re-detection. It is not disagreement, and
|
|
58864
|
+
* counting it as such would punish a candidate for the tracker's own warm-up.
|
|
58865
|
+
* An ABSENT verdict likewise falls back to overlap alone: a path that never had
|
|
58866
|
+
* an opinion must not be silently reclassified.
|
|
58867
|
+
*/
|
|
58868
|
+
function observationAgrees(overlap, agreeIou, movementState) {
|
|
58869
|
+
if (overlap < agreeIou) return false;
|
|
58870
|
+
return movementState !== "moving" && movementState !== "left";
|
|
58871
|
+
}
|
|
58872
|
+
/**
|
|
58864
58873
|
* Fold one observation into a candidate.
|
|
58865
58874
|
*
|
|
58866
58875
|
* `agrees` is decided by the CALLER, because M is what makes the ratio mean
|
|
@@ -59163,7 +59172,7 @@ var StationaryEvidenceLedger = class {
|
|
|
59163
59172
|
* slow or failing store can never change what this frame decides. The
|
|
59164
59173
|
* durable half is fired behind the verdict.
|
|
59165
59174
|
*/
|
|
59166
|
-
observe(observation
|
|
59175
|
+
observe(observation) {
|
|
59167
59176
|
const policy = evidencePolicyFor(observation.className);
|
|
59168
59177
|
this.expire(observation.deviceId, observation.timestamp);
|
|
59169
59178
|
const bindKey = observation.trackId === void 0 ? void 0 : `${observation.deviceId}:${observation.trackId}`;
|
|
@@ -59211,18 +59220,18 @@ var StationaryEvidenceLedger = class {
|
|
|
59211
59220
|
this.capDevice(observation.deviceId);
|
|
59212
59221
|
return {
|
|
59213
59222
|
row,
|
|
59214
|
-
verdict: evaluateStationaryEvidence(row, policy
|
|
59223
|
+
verdict: evaluateStationaryEvidence(row, policy),
|
|
59215
59224
|
promoted: false
|
|
59216
59225
|
};
|
|
59217
59226
|
}
|
|
59218
59227
|
if (bindKey !== void 0) this.binding.set(bindKey, row.key);
|
|
59219
59228
|
}
|
|
59220
|
-
const agrees = overlap
|
|
59229
|
+
const agrees = observationAgrees(overlap, policy.agreeIou, observation.movementState);
|
|
59221
59230
|
const next = foldObservation(row, observation, agrees, overlap);
|
|
59222
59231
|
this.ledger.stage(next);
|
|
59223
59232
|
this.dirty.add(next.key);
|
|
59224
59233
|
if (created) this.capDevice(observation.deviceId);
|
|
59225
|
-
const verdict = evaluateStationaryEvidence(next, policy
|
|
59234
|
+
const verdict = evaluateStationaryEvidence(next, policy);
|
|
59226
59235
|
if (verdict.kind !== "promote") return {
|
|
59227
59236
|
row: next,
|
|
59228
59237
|
verdict,
|
|
@@ -59670,8 +59679,46 @@ var StationaryObjectRegistry = class {
|
|
|
59670
59679
|
});
|
|
59671
59680
|
}
|
|
59672
59681
|
}
|
|
59673
|
-
/**
|
|
59674
|
-
|
|
59682
|
+
/**
|
|
59683
|
+
* Promote a parked track into a persisted stationary entry — or, when the
|
|
59684
|
+
* spot is already censused, CONFIRM the entry sitting on it.
|
|
59685
|
+
*
|
|
59686
|
+
* The occupancy question is asked here and nowhere else, with the same
|
|
59687
|
+
* predicate the per-frame filter uses (`findParkedOccupant`): a box the
|
|
59688
|
+
* filter would suppress as a parked object's own can never become a second
|
|
59689
|
+
* parked object. Until 2026-09-14 this method minted whatever it was handed,
|
|
59690
|
+
* and device 617 reported EIGHT parked vehicles — four of them one car, the
|
|
59691
|
+
* same box to the decimal, promoted again roughly once an hour because the
|
|
59692
|
+
* weak-continuation path (D410) keeps feeding the ledger from a track riding
|
|
59693
|
+
* a parked object. The re-promotion is not the anomaly and is not worth
|
|
59694
|
+
* suppressing: it is a fresh, independent confirmation that the thing is
|
|
59695
|
+
* still there. It just must not produce a neighbour.
|
|
59696
|
+
*
|
|
59697
|
+
* A merge never DOWNGRADES the tenant: it advances the clock and adopts an
|
|
59698
|
+
* identity the tenant lacks (the 617 duplicates arrived unlabelled while the
|
|
59699
|
+
* row they belonged to was already `Car`), and it never overwrites one it
|
|
59700
|
+
* has.
|
|
59701
|
+
*/
|
|
59702
|
+
async promote(entry, config) {
|
|
59703
|
+
const occupant = findParkedOccupant(this.list(entry.deviceId), entry.className, entry.bbox, (config ?? this.matchConfig).suppressIou, (config ?? this.matchConfig).wakeAssocIou);
|
|
59704
|
+
if (occupant !== void 0) {
|
|
59705
|
+
const merged = this.absorbInto(occupant, entry);
|
|
59706
|
+
this.logger.info("stationary promotion absorbed — the spot is already censused", {
|
|
59707
|
+
tags: { deviceId: occupant.deviceId },
|
|
59708
|
+
meta: {
|
|
59709
|
+
deviceId: occupant.deviceId,
|
|
59710
|
+
entryId: occupant.id,
|
|
59711
|
+
className: occupant.className,
|
|
59712
|
+
refusedTrackId: entry.sourceTrackId,
|
|
59713
|
+
parkedForMs: entry.becameStationaryAt - occupant.becameStationaryAt,
|
|
59714
|
+
gainedLabel: occupant.label === void 0 && entry.label !== void 0
|
|
59715
|
+
}
|
|
59716
|
+
});
|
|
59717
|
+
return {
|
|
59718
|
+
outcome: "merged",
|
|
59719
|
+
entry: merged
|
|
59720
|
+
};
|
|
59721
|
+
}
|
|
59675
59722
|
this.indexEntry(entry.deviceId, entry.id);
|
|
59676
59723
|
this.dirty.delete(entry.id);
|
|
59677
59724
|
await this.ledger.put(entry);
|
|
@@ -59680,6 +59727,22 @@ var StationaryObjectRegistry = class {
|
|
|
59680
59727
|
entry,
|
|
59681
59728
|
timestamp: entry.becameStationaryAt
|
|
59682
59729
|
});
|
|
59730
|
+
return {
|
|
59731
|
+
outcome: "born",
|
|
59732
|
+
entry
|
|
59733
|
+
};
|
|
59734
|
+
}
|
|
59735
|
+
/** Fold a refused promotion into the entry that already holds its spot. */
|
|
59736
|
+
absorbInto(occupant, refused) {
|
|
59737
|
+
const merged = {
|
|
59738
|
+
...occupant,
|
|
59739
|
+
lastConfirmedAt: Math.max(occupant.lastConfirmedAt, refused.becameStationaryAt),
|
|
59740
|
+
...occupant.label === void 0 && refused.label !== void 0 ? { label: refused.label } : {},
|
|
59741
|
+
...occupant.keyFrameMediaId === void 0 && refused.keyFrameMediaId !== void 0 ? { keyFrameMediaId: refused.keyFrameMediaId } : {}
|
|
59742
|
+
};
|
|
59743
|
+
this.ledger.stage(merged);
|
|
59744
|
+
this.dirty.add(merged.id);
|
|
59745
|
+
return merged;
|
|
59683
59746
|
}
|
|
59684
59747
|
/**
|
|
59685
59748
|
* Retire entries unconfirmed for longer than the TTL of OBSERVED time, and
|
|
@@ -59767,6 +59830,7 @@ var StationaryObjectRegistry = class {
|
|
|
59767
59830
|
}
|
|
59768
59831
|
});
|
|
59769
59832
|
this.strikeCountersByDevice.clear();
|
|
59833
|
+
for (const deviceId of [...this.idsByDevice.keys()]) this.collapseDuplicates(deviceId);
|
|
59770
59834
|
for (const id of [...this.dirty]) {
|
|
59771
59835
|
this.dirty.delete(id);
|
|
59772
59836
|
const entry = this.ledger.get(id);
|
|
@@ -59908,6 +59972,46 @@ var StationaryObjectRegistry = class {
|
|
|
59908
59972
|
/** Retire one entry: index, staged advance, pending strike and durable row
|
|
59909
59973
|
* all go. A strike that outlived its entry would arm the NEXT entry to reuse
|
|
59910
59974
|
* the id — the registry mints UUIDs, but the invariant is cheap to keep. */
|
|
59975
|
+
/**
|
|
59976
|
+
* Fold every row that holds another row's spot into the OLDEST of them.
|
|
59977
|
+
*
|
|
59978
|
+
* The survivor is the oldest because it is the one carrying the object's real
|
|
59979
|
+
* `becameStationaryAt` — "parked since 14:30" is the fact the operator reads,
|
|
59980
|
+
* and the duplicates are all later re-discoveries of the same thing. It
|
|
59981
|
+
* inherits any identity the losers carry and that it lacks, and the losers'
|
|
59982
|
+
* rows are deleted outright: a retirement would announce a departure that
|
|
59983
|
+
* never happened.
|
|
59984
|
+
*
|
|
59985
|
+
* Uses the same predicate as the filter and the birth guard, so a collapse
|
|
59986
|
+
* can only ever remove a row that could not have been independently confirmed
|
|
59987
|
+
* anyway.
|
|
59988
|
+
*/
|
|
59989
|
+
collapseDuplicates(deviceId) {
|
|
59990
|
+
const entries = [...this.list(deviceId)].sort((a, b) => a.becameStationaryAt - b.becameStationaryAt);
|
|
59991
|
+
if (entries.length < 2) return;
|
|
59992
|
+
const survivors = [];
|
|
59993
|
+
for (const candidate of entries) {
|
|
59994
|
+
const occupant = findParkedOccupant(survivors, candidate.className, candidate.bbox, this.matchConfig.suppressIou, this.matchConfig.wakeAssocIou);
|
|
59995
|
+
if (occupant === void 0) {
|
|
59996
|
+
survivors.push(candidate);
|
|
59997
|
+
continue;
|
|
59998
|
+
}
|
|
59999
|
+
const merged = this.absorbInto(occupant, candidate);
|
|
60000
|
+
survivors[survivors.indexOf(occupant)] = merged;
|
|
60001
|
+
this.dropEntry(deviceId, candidate.id);
|
|
60002
|
+
this.logger.info("stationary duplicate collapsed — two rows, one spot", {
|
|
60003
|
+
tags: { deviceId },
|
|
60004
|
+
meta: {
|
|
60005
|
+
deviceId,
|
|
60006
|
+
keptEntryId: occupant.id,
|
|
60007
|
+
droppedEntryId: candidate.id,
|
|
60008
|
+
className: candidate.className,
|
|
60009
|
+
keptSinceMs: occupant.becameStationaryAt,
|
|
60010
|
+
droppedSinceMs: candidate.becameStationaryAt
|
|
60011
|
+
}
|
|
60012
|
+
});
|
|
60013
|
+
}
|
|
60014
|
+
}
|
|
59911
60015
|
dropEntry(deviceId, id) {
|
|
59912
60016
|
this.idsByDevice.get(deviceId)?.delete(id);
|
|
59913
60017
|
this.dirty.delete(id);
|
|
@@ -69950,18 +70054,6 @@ var StationarySettingsSchema = require_dist.object({
|
|
|
69950
70054
|
* session open (also the minimum track age). Creates nothing — birth is
|
|
69951
70055
|
* decided by repeated evidence. `SESSION_STILLNESS_WINDOW_MS`. */
|
|
69952
70056
|
sessionStillnessWindowMs: require_dist.number().int().min(5e3).max(12e4).default(SESSION_STILLNESS_WINDOW_MS),
|
|
69953
|
-
/**
|
|
69954
|
-
* Let a motionless PERSON become a stationary entry on this camera.
|
|
69955
|
-
*
|
|
69956
|
-
* Off, and it is a measurement rather than caution. Replaying 24 h of person
|
|
69957
|
-
* observations from devices 617/1443/590/592 through the evidence ledger
|
|
69958
|
-
* produces 6-22 entries a day depending on how strictly it is tuned, and none
|
|
69959
|
-
* of them can be tuned to zero: device 592 alone yielded eleven candidates at
|
|
69960
|
-
* a perfect agreement ratio between 02:00 and 05:00. A stationary entry
|
|
69961
|
-
* SUPPRESSES its object, so a wrong one makes the camera blind to a person —
|
|
69962
|
-
* turn this on only for a camera where you know what is standing there.
|
|
69963
|
-
*/
|
|
69964
|
-
promotePersons: require_dist.boolean().default(false),
|
|
69965
70057
|
/** Observed-time TTL: retire an entry unconfirmed for this long (measured on
|
|
69966
70058
|
* frames-flowing time, not wall-clock). `ENTRY_TTL_MS`. */
|
|
69967
70059
|
entryTtlMs: require_dist.number().int().min(6e4).max(18e5).default(ENTRY_TTL_MS)
|
|
@@ -69981,7 +70073,6 @@ function resolveStationarySettings(raw) {
|
|
|
69981
70073
|
suppressIou: pick("suppressIou"),
|
|
69982
70074
|
wakeAssocIou: pick("wakeAssocIou"),
|
|
69983
70075
|
sessionStillnessWindowMs: pick("sessionStillnessWindowMs"),
|
|
69984
|
-
promotePersons: pick("promotePersons"),
|
|
69985
70076
|
entryTtlMs: pick("entryTtlMs")
|
|
69986
70077
|
};
|
|
69987
70078
|
}
|
|
@@ -70517,13 +70608,6 @@ function buildDetectionSettingsSections() {
|
|
|
70517
70608
|
unit: "s",
|
|
70518
70609
|
displayScale: 1e3
|
|
70519
70610
|
},
|
|
70520
|
-
{
|
|
70521
|
-
type: "boolean",
|
|
70522
|
-
key: "promotePersons",
|
|
70523
|
-
label: "Also park motionless people",
|
|
70524
|
-
description: "Off by default. A stationary entry SUPPRESSES its object, so a wrong one makes this camera blind to a person standing there. Replaying a day of real person detections produced entries on every camera tried, at every threshold tried — turn this on only where you know what is standing still.",
|
|
70525
|
-
default: STATIONARY_DEFAULTS.promotePersons
|
|
70526
|
-
},
|
|
70527
70611
|
{
|
|
70528
70612
|
type: "slider",
|
|
70529
70613
|
key: "entryTtlMs",
|
|
@@ -73867,11 +73951,7 @@ var ZoneAnalyticsProvider = class {
|
|
|
73867
73951
|
});
|
|
73868
73952
|
}
|
|
73869
73953
|
this.snapshots.set(input.deviceId, snapshot);
|
|
73870
|
-
|
|
73871
|
-
if (noteOccupancy !== void 0) {
|
|
73872
|
-
noteOccupancy(input.deviceId, total, input.timestamp);
|
|
73873
|
-
for (const zone of snapshot.zones) noteOccupancy(input.deviceId, zone.totalObjects, input.timestamp, zone.zoneId);
|
|
73874
|
-
}
|
|
73954
|
+
this.noteLtsSamples(input.deviceId, snapshot, input.timestamp);
|
|
73875
73955
|
this.appendHistory(input.deviceId, snapshot);
|
|
73876
73956
|
this.emitSnapshot(input.deviceId, snapshot);
|
|
73877
73957
|
this.sliceThrottle.push(input.deviceId, snapshot);
|
|
@@ -73938,11 +74018,30 @@ var ZoneAnalyticsProvider = class {
|
|
|
73938
74018
|
});
|
|
73939
74019
|
if (snapshot) {
|
|
73940
74020
|
this.appendHistory(deviceId, snapshot);
|
|
74021
|
+
this.noteLtsSamples(deviceId, snapshot, now);
|
|
73941
74022
|
this.emitSnapshot(deviceId, snapshot);
|
|
73942
74023
|
this.sliceThrottle.push(deviceId, snapshot);
|
|
73943
74024
|
}
|
|
73944
74025
|
}
|
|
73945
74026
|
}
|
|
74027
|
+
/**
|
|
74028
|
+
* Append this snapshot to the long-term occupancy series — frame-wide first
|
|
74029
|
+
* (`scope: ''`), then one sample per zone.
|
|
74030
|
+
*
|
|
74031
|
+
* Shared by BOTH producers on purpose. It used to live inline in
|
|
74032
|
+
* `recordFrame`, so the baseline sampler — the only producer a camera with no
|
|
74033
|
+
* frames flowing has — wrote the RAM ring, the NC watcher and the state slice
|
|
74034
|
+
* but never LTS. The long-term series therefore had a hole exactly where the
|
|
74035
|
+
* scene was nothing but parked objects: the one span an occupancy history is
|
|
74036
|
+
* asked about. The RAM ring showed a flat line over the same minutes, which
|
|
74037
|
+
* is what made the gap invisible.
|
|
74038
|
+
*/
|
|
74039
|
+
noteLtsSamples(deviceId, snapshot, timestamp) {
|
|
74040
|
+
const note = this.ctx.onOccupancySample;
|
|
74041
|
+
if (note === void 0) return;
|
|
74042
|
+
note(deviceId, snapshot.frame.totalObjects, timestamp);
|
|
74043
|
+
for (const zone of snapshot.zones) note(deviceId, zone.totalObjects, timestamp, zone.zoneId);
|
|
74044
|
+
}
|
|
73946
74045
|
/** Push one snapshot to the occupancy tap, isolating any consumer throw from
|
|
73947
74046
|
* the frame path (occupancy notifications must never break analytics). */
|
|
73948
74047
|
emitSnapshot(deviceId, snapshot) {
|
|
@@ -78277,7 +78376,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
78277
78376
|
}
|
|
78278
78377
|
});
|
|
78279
78378
|
}
|
|
78280
|
-
await this.processFrame(deviceId, frame, data.source ?? "pipeline", data.frameHandle, data.detailSteps);
|
|
78379
|
+
await this.processFrame(deviceId, frame, data.source ?? "pipeline", data.frameHandle, data.detailSteps, data.occasion ?? "session");
|
|
78281
78380
|
}
|
|
78282
78381
|
/**
|
|
78283
78382
|
* Run one detection frame through the analysis layers for a given
|
|
@@ -78287,7 +78386,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
78287
78386
|
* tracking/zone/event state never crosses between sources. Emits the
|
|
78288
78387
|
* SAME canonical events, distinguished only by `source`.
|
|
78289
78388
|
*/
|
|
78290
|
-
async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps) {
|
|
78389
|
+
async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps, occasion = "session") {
|
|
78291
78390
|
if (this.shuttingDown) return;
|
|
78292
78391
|
if (!await this.bindingCache.isActive(deviceId)) {
|
|
78293
78392
|
const now = Date.now();
|
|
@@ -78695,7 +78794,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
78695
78794
|
const evidence = this.stationaryEvidence;
|
|
78696
78795
|
for (const t of result.tracked) {
|
|
78697
78796
|
if (packageClasses.has(t.className)) continue;
|
|
78698
|
-
const classEnabled = t.className === "person" ? stationarySettings.promotePersons : evidencePolicyFor(t.className).promotesByDefault;
|
|
78699
78797
|
const active = this.trackStore?.peekActive(t.trackId) ?? null;
|
|
78700
78798
|
const observedLabel = active === null ? void 0 : displayLabel(active);
|
|
78701
78799
|
const observedKeyFrame = this.residents.keyFrameMediaId(t.trackId);
|
|
@@ -78704,12 +78802,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
78704
78802
|
className: t.className,
|
|
78705
78803
|
bbox: { ...t.bbox },
|
|
78706
78804
|
timestamp: result.timestamp,
|
|
78707
|
-
source:
|
|
78805
|
+
source: occasion,
|
|
78708
78806
|
trackId: t.trackId,
|
|
78709
78807
|
...observedKeyFrame !== void 0 ? { keyFrameMediaId: observedKeyFrame } : {},
|
|
78710
|
-
...observedLabel !== void 0 ? { label: observedLabel } : {}
|
|
78808
|
+
...observedLabel !== void 0 ? { label: observedLabel } : {},
|
|
78809
|
+
movementState: t.state
|
|
78711
78810
|
};
|
|
78712
|
-
const outcome = evidence.observe(observation
|
|
78811
|
+
const outcome = evidence.observe(observation);
|
|
78713
78812
|
if (outcome === void 0 || !outcome.promoted) continue;
|
|
78714
78813
|
this.birthStationaryFromEvidence({
|
|
78715
78814
|
deviceId,
|
|
@@ -81812,7 +81911,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
81812
81911
|
...label !== void 0 ? { label } : {},
|
|
81813
81912
|
...keyFrameMediaId !== void 0 ? { keyFrameMediaId } : {}
|
|
81814
81913
|
};
|
|
81815
|
-
if (plausible)
|
|
81914
|
+
if (plausible) {
|
|
81915
|
+
const gate = this.stationarySettingsFromCache(deviceId);
|
|
81916
|
+
this.stationaryRegistry?.promote(entry, {
|
|
81917
|
+
suppressIou: gate.suppressIou,
|
|
81918
|
+
wakeAssocIou: gate.wakeAssocIou
|
|
81919
|
+
});
|
|
81920
|
+
}
|
|
81816
81921
|
processor.dropTrack(track.trackId);
|
|
81817
81922
|
this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
|
|
81818
81923
|
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|