@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as
|
|
1
|
+
import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as readTimelapseGeneratedAt, B as NcTaxonomySchema, Bt as errMsg$1, C as MOTION_CLOSE_AFTER_MS, Ct as mayWriteToLocation, D as NC_DEFAULT_SNOOZE_MINUTES, Dt as pipelineAnalyticsCapability, E as NC_CONDITION_CATALOG, Et as pickClusterStepModels, F as NcScheduleSchema, Ft as subKindsOf, Gt as customAction, Ht as CamProfileSchema, I as NcSnoozeInputSchema, It as systemEventFilterApplies, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as defineCustomActions, L as NcSnoozeSchema, Lt as vectorDimFromBase64, M as NcRulePatchSchema, N as NcRuleSchema, Nt as sceneMonitorCapability, Ot as plateGalleryCapability, P as NcRuleTargetSchema, Pt as storageOccupancyCapability, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as videoclipsCapability, St as kebabToCamel, T as NC_ALARM_SYSTEM_EVENT_KINDS, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as DeviceType, V as OpsLogEntrySchema, Vt as BaseAddon, W as RetrainStatusSchema, Wt as createEvent, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isScheduleActive, cn as EventCategory, ct as cosineSimilarity$1, dt as encodeVectorBase64, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, ft as evaluateSensorEdge, g as FIRST_LEVEL_MACRO_CLASSES, h as EVENT_PAD_MS, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as resolveLocationMode, k as NC_TAXONOMY, kt as readDeviceStateFrom, lt as deriveRecordingMode, m as EVENT_OWNER_TYPES, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, v as FailureCounters, vt as isEventOwnerType, w as MediaFileKindEnum, wt as notificationRulesCapability, x as MAX_BIRTH_DECISION_RECORDS, xt as isSourceCap, y as LabelAttributionSchema, yt as isMediaOwnerType, z as NcSystemEventKindSchema, zt as zoneAnalyticsCapability } from "../dist-C-1xTyB-.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -55551,7 +55551,8 @@ function buildOverlayDetections(input) {
|
|
|
55551
55551
|
...det,
|
|
55552
55552
|
track: {
|
|
55553
55553
|
trackId: t.trackId,
|
|
55554
|
-
zones: t.zones
|
|
55554
|
+
zones: t.zones,
|
|
55555
|
+
...t.state !== void 0 ? { movementState: t.state } : {}
|
|
55555
55556
|
},
|
|
55556
55557
|
labels: hasName && !alreadyLabeled ? [...det.labels, {
|
|
55557
55558
|
label: name,
|
|
@@ -57509,6 +57510,10 @@ function centroidInside(b, p) {
|
|
|
57509
57510
|
}
|
|
57510
57511
|
/** Parked-box coverage (intersection / entry area) that counts as swallowed. */
|
|
57511
57512
|
var SWALLOW_MIN_COVERAGE = .5;
|
|
57513
|
+
/** Fraction of a detection's OWN area that must lie inside a parked entry for
|
|
57514
|
+
* the entry to swallow it. Deliberately near-total: a fragment of a parked
|
|
57515
|
+
* object does not stick out of it. */
|
|
57516
|
+
var FRAGMENT_MIN_CONTAINMENT = .9;
|
|
57512
57517
|
function isSceneSizedBox(bbox, frameWidth, frameHeight) {
|
|
57513
57518
|
const frame = frameWidth * frameHeight;
|
|
57514
57519
|
return frame > 0 && bbox.w * bbox.h / frame >= .55;
|
|
@@ -57528,6 +57533,29 @@ function detectionSwallowsEntry(det, parked) {
|
|
|
57528
57533
|
if (!centroidInside(det, centroid(parked))) return false;
|
|
57529
57534
|
return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
|
|
57530
57535
|
}
|
|
57536
|
+
/**
|
|
57537
|
+
* True when `det` is a FRAGMENT of the parked object — much smaller than the
|
|
57538
|
+
* entry and sitting (almost) wholly inside it.
|
|
57539
|
+
*
|
|
57540
|
+
* The mirror of {@link detectionSwallowsEntry}, and the case neither IoU
|
|
57541
|
+
* threshold can see. Measured on 617: a 34x137 box inside a 452x420 parked
|
|
57542
|
+
* motorcycle scores IoU 0.024 — BELOW `wakeAssocIou` (0.1), so the filter read
|
|
57543
|
+
* it as "no detection overlaps this entry" and let it open a track, accumulate
|
|
57544
|
+
* its own evidence and promote into a registry entry of its own. The camera
|
|
57545
|
+
* then reported two parked vehicles where there was one motorcycle.
|
|
57546
|
+
*
|
|
57547
|
+
* The size bound is the reciprocal of {@link SWALLOW_AREA_RATIO}, so the two
|
|
57548
|
+
* swallow rules meet without overlapping: a box between 1/1.35 and 1.35 of the
|
|
57549
|
+
* entry's area, contained, already scores IoU >= 0.74 and is answered by
|
|
57550
|
+
* `suppressIou`. Nothing is a fragment and a near-identical box at once.
|
|
57551
|
+
*/
|
|
57552
|
+
function entrySwallowsDetection(parked, det) {
|
|
57553
|
+
const entryArea = parked.w * parked.h;
|
|
57554
|
+
const detArea = det.w * det.h;
|
|
57555
|
+
if (entryArea <= 0 || detArea <= 0) return false;
|
|
57556
|
+
if (detArea > entryArea / 1.35) return false;
|
|
57557
|
+
return intersectionArea$2(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
|
|
57558
|
+
}
|
|
57531
57559
|
function parkedEntryHoldsDetection(entry, detBbox, config) {
|
|
57532
57560
|
const overlap = iou(entry.bbox, detBbox);
|
|
57533
57561
|
if (overlap >= config.suppressIou) return {
|
|
@@ -57542,6 +57570,10 @@ function parkedEntryHoldsDetection(entry, detBbox, config) {
|
|
|
57542
57570
|
strong: false,
|
|
57543
57571
|
holds: false
|
|
57544
57572
|
};
|
|
57573
|
+
if (entrySwallowsDetection(entry.bbox, detBbox)) return {
|
|
57574
|
+
strong: false,
|
|
57575
|
+
holds: true
|
|
57576
|
+
};
|
|
57545
57577
|
return {
|
|
57546
57578
|
strong: false,
|
|
57547
57579
|
holds: overlap >= config.wakeAssocIou && centroidInside(entry.bbox, centroid(detBbox))
|
|
@@ -57560,6 +57592,34 @@ function bboxMatchesParkedEntry(bbox, className, entries, suppressIou = SUPPRESS
|
|
|
57560
57592
|
}
|
|
57561
57593
|
return false;
|
|
57562
57594
|
}
|
|
57595
|
+
/**
|
|
57596
|
+
* The live entry already censusing this spot, if any — the ONE answer to "is
|
|
57597
|
+
* this place taken?", asked at BIRTH.
|
|
57598
|
+
*
|
|
57599
|
+
* It is deliberately the same predicate the per-frame filter uses
|
|
57600
|
+
* ({@link parkedEntryHoldsDetection}): a box the filter would suppress as a
|
|
57601
|
+
* parked object's own must not be able to become a second parked object. Device
|
|
57602
|
+
* 617 held four rows for one car and a fifth for a fragment of a motorcycle
|
|
57603
|
+
* because the birth path asked nothing at all.
|
|
57604
|
+
*/
|
|
57605
|
+
function findParkedOccupant(entries, className, bbox, suppressIou = SUPPRESS_IOU, wakeAssocIou = WAKE_ASSOC_IOU) {
|
|
57606
|
+
const cls = normaliseStationaryClass(className);
|
|
57607
|
+
const config = {
|
|
57608
|
+
suppressIou,
|
|
57609
|
+
wakeAssocIou
|
|
57610
|
+
};
|
|
57611
|
+
let best;
|
|
57612
|
+
for (const e of entries) {
|
|
57613
|
+
if (normaliseStationaryClass(e.className) !== cls) continue;
|
|
57614
|
+
if (!parkedEntryHoldsDetection(e, bbox, config).holds) continue;
|
|
57615
|
+
const overlap = iou(e.bbox, bbox);
|
|
57616
|
+
if (best === void 0 || overlap > best.overlap) best = {
|
|
57617
|
+
entry: e,
|
|
57618
|
+
overlap
|
|
57619
|
+
};
|
|
57620
|
+
}
|
|
57621
|
+
return best?.entry;
|
|
57622
|
+
}
|
|
57563
57623
|
/** Best-shot veto: current crop is a parked object the trail did not start on. */
|
|
57564
57624
|
function thumbnailWouldShowParkedNotSubject(input) {
|
|
57565
57625
|
if (!bboxMatchesParkedEntry(input.currentBbox, input.className, input.entries)) return false;
|
|
@@ -58545,85 +58605,6 @@ var StartedBirthRegistry = class {
|
|
|
58545
58605
|
//#endregion
|
|
58546
58606
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
|
|
58547
58607
|
/**
|
|
58548
|
-
* stationary-evidence — how a parked object EARNS a registry entry.
|
|
58549
|
-
*
|
|
58550
|
-
* ## The criterion this replaces, and why it was measuring the wrong thing
|
|
58551
|
-
*
|
|
58552
|
-
* Until 2026-08-28 an object became stationary by TRACK CONTINUITY: one track
|
|
58553
|
-
* had to stay alive and barely move for `PROMOTION_WINDOW_MS` (30 s). That
|
|
58554
|
-
* rewards an object that is *seen well and often* — precisely the opposite of
|
|
58555
|
-
* what the operator needs. Measured on device 617 (the parking lot), 24 h:
|
|
58556
|
-
*
|
|
58557
|
-
* - three cars are parked in view; the registry held exactly ONE entry;
|
|
58558
|
-
* - the two it missed were re-discovered as **140 and 112 separate tracks**,
|
|
58559
|
-
* median life **6.8 s**, p90 29 s. Not one of them could ever reach 30 s.
|
|
58560
|
-
* - those two objects alone produced **13 494 of 18 402** vehicle
|
|
58561
|
-
* observations (73%) — the whole re-spawn flood the registry exists to stop.
|
|
58562
|
-
*
|
|
58563
|
-
* The track dies; the OBJECT does not. So the evidence must outlive the track.
|
|
58564
|
-
*
|
|
58565
|
-
* ## The new criterion: a RATIO, not a count
|
|
58566
|
-
*
|
|
58567
|
-
* "The same thing, in the same place, most of the times it was seen."
|
|
58568
|
-
*
|
|
58569
|
-
* N — observations that AGREED with the candidate's anchor box (IoU ≥
|
|
58570
|
-
* {@link StationaryEvidencePolicy.agreeIou}).
|
|
58571
|
-
* M — observations OFFERED by a track once it bound to this candidate,
|
|
58572
|
-
* agreeing or not.
|
|
58573
|
-
*
|
|
58574
|
-
* A count would rescue nothing: a badly-lit car at night is seen rarely, and
|
|
58575
|
-
* any floor high enough to exclude a passer-by would exclude it too. A RATIO
|
|
58576
|
-
* asks a question a rarely-seen object can still answer well — 8 sightings out
|
|
58577
|
-
* of 10 in the same spot is stationary even though 10 is a small number.
|
|
58578
|
-
*
|
|
58579
|
-
* The dilution is the whole point, and it is why M counts a bound track's
|
|
58580
|
-
* observations even after it walks away: a car that drives THROUGH a spot binds
|
|
58581
|
-
* for a few frames and then contributes dozens of disagreeing ones. Measured on
|
|
58582
|
-
* 617 the two populations do not overlap:
|
|
58583
|
-
*
|
|
58584
|
-
* | candidate | N / M | ratio |
|
|
58585
|
-
* | ---------------------------- | ----------- | ----- |
|
|
58586
|
-
* | parked car A (missed today) | 6405 / 6467 | 0.99 |
|
|
58587
|
-
* | parked car B (missed today) | 343 / 343 | 1.00 |
|
|
58588
|
-
* | car crossing the lot | 5 / 70 | 0.07 |
|
|
58589
|
-
* | car turning through the lot | 14 / 91 | 0.15 |
|
|
58590
|
-
* | car manoeuvring | 12 / 41 | 0.29 |
|
|
58591
|
-
*
|
|
58592
|
-
* The threshold sits in the empty band between 0.29 and 0.99.
|
|
58593
|
-
*
|
|
58594
|
-
* ## The span is what a ratio cannot say
|
|
58595
|
-
*
|
|
58596
|
-
* A ratio alone promotes a car that paused for four seconds — its handful of
|
|
58597
|
-
* observations all agree. So evidence must also STRETCH: the agreeing
|
|
58598
|
-
* observations must span {@link StationaryEvidencePolicy.minSpanMs}. Measured
|
|
58599
|
-
* on 617, real parked cars span 2 min to 24 h; the two momentary stops in the
|
|
58600
|
-
* same day spanned 4.2 s and 8.0 s. Setting the span to
|
|
58601
|
-
* `DEFAULT_MAX_SESSION_HOLD_MS` also buys a structural guarantee: no single
|
|
58602
|
-
* motion session, however long it is held open, can promote on its own.
|
|
58603
|
-
*
|
|
58604
|
-
* ## ONE ledger, two producers
|
|
58605
|
-
*
|
|
58606
|
-
* Evidence is anchored to a BOX, not to a track and not to a session, so an
|
|
58607
|
-
* object confirmed half by an active motion session and half by a periodic
|
|
58608
|
-
* burst still promotes. Occasion A (the session) is implemented and is
|
|
58609
|
-
* sufficient on its own — it is free, because the pipeline is already decoding
|
|
58610
|
-
* and already inferring. Occasion B (the burst) is a precaution for the cases
|
|
58611
|
-
* where A could not have fired at all (hub restarted, camera offline, motion
|
|
58612
|
-
* detection failed, object already parked at boot); it is NOT implemented yet,
|
|
58613
|
-
* and {@link StationaryEvidenceSource} is the seam it will arrive through.
|
|
58614
|
-
*
|
|
58615
|
-
* ## Identity is IoU. There is no grid.
|
|
58616
|
-
*
|
|
58617
|
-
* The predecessor mechanism (`phantom-cells.ts`) quantises a point to a 32 px
|
|
58618
|
-
* cell, and its write path passes a CENTROID while its read path passes a
|
|
58619
|
-
* top-left CORNER — at 32 px every box bigger than 32 px lands in a different
|
|
58620
|
-
* cell on read, and its unit test never crosses that boundary because it calls
|
|
58621
|
-
* both halves with the same literal coordinates. This module has no such
|
|
58622
|
-
* boundary to get wrong: a candidate is found by overlap against the boxes a
|
|
58623
|
-
* device already holds, bounded by {@link EVIDENCE_MAX_CANDIDATES_PER_DEVICE}.
|
|
58624
|
-
* Device 617 — the busiest lot in the fleet — holds 8.
|
|
58625
|
-
*/
|
|
58626
|
-
/**
|
|
58627
58608
|
* `vehicle` and its kin. Every number below is read off device 617 over 24 h
|
|
58628
58609
|
* (18 402 vehicle observations, 290 tracks) with the three parked cars the
|
|
58629
58610
|
* operator can see out of the window as the bench.
|
|
@@ -58645,11 +58626,11 @@ var VEHICLE_EVIDENCE_POLICY = {
|
|
|
58645
58626
|
agreeIou: .5,
|
|
58646
58627
|
minAgreed: 8,
|
|
58647
58628
|
minAgreementRatio: .6,
|
|
58648
|
-
minSpanMs: 12e4
|
|
58649
|
-
promotesByDefault: true
|
|
58629
|
+
minSpanMs: 12e4
|
|
58650
58630
|
};
|
|
58651
58631
|
/**
|
|
58652
|
-
* `person`, and
|
|
58632
|
+
* `person`, promoted like every other class since 2026-09-14 — and still the
|
|
58633
|
+
* strictest numbers in the file, because these are MEASURED, not cautious.
|
|
58653
58634
|
*
|
|
58654
58635
|
* This is not caution, it is a measurement. Replaying 24 h of person
|
|
58655
58636
|
* observations from devices 617, 1443, 590 and 592 through this exact ledger:
|
|
@@ -58668,17 +58649,24 @@ var VEHICLE_EVIDENCE_POLICY = {
|
|
|
58668
58649
|
*
|
|
58669
58650
|
* Promoting a person is a BLINDING decision (D49 read in the direction that
|
|
58670
58651
|
* destroys work: for a car the destructive read is the RETIREMENT — the flood
|
|
58671
|
-
* comes back; for a person it is the BIRTH — we stop seeing them).
|
|
58672
|
-
*
|
|
58673
|
-
*
|
|
58674
|
-
*
|
|
58652
|
+
* comes back; for a person it is the BIRTH — we stop seeing them). It used to
|
|
58653
|
+
* ship OFF behind a per-camera switch for that reason; the switch is gone —
|
|
58654
|
+
* an opt-in nobody turns on produces nothing, and it was producing nothing.
|
|
58655
|
+
*
|
|
58656
|
+
* What makes that affordable now is that agreement is no longer overlap alone:
|
|
58657
|
+
* `observationAgrees` requires the TRACKER to call the subject still, so a
|
|
58658
|
+
* person who merely pauses inside the anchor no longer feeds the candidate.
|
|
58659
|
+
* The statue and the poster still promote — which is what you want, they ARE
|
|
58660
|
+
* furniture — while the real person who stops for a moment no longer does.
|
|
58661
|
+
*
|
|
58662
|
+
* The thresholds stay where the measurement put them. They are the difference
|
|
58663
|
+
* between 22 promotions a day and 10.
|
|
58675
58664
|
*/
|
|
58676
58665
|
var PERSON_EVIDENCE_POLICY = {
|
|
58677
58666
|
agreeIou: .5,
|
|
58678
58667
|
minAgreed: 40,
|
|
58679
58668
|
minAgreementRatio: .95,
|
|
58680
|
-
minSpanMs: 18e5
|
|
58681
|
-
promotesByDefault: false
|
|
58669
|
+
minSpanMs: 18e5
|
|
58682
58670
|
};
|
|
58683
58671
|
/** Classes with no policy of their own fall back to the vehicle numbers —
|
|
58684
58672
|
* `animal`, `package` and friends are all "an object that ended up somewhere",
|
|
@@ -58697,7 +58685,7 @@ function stationaryEvidenceKey(deviceId, className, anchorId) {
|
|
|
58697
58685
|
* Pure, and ordered cheapest-question-first so the reason a candidate was
|
|
58698
58686
|
* refused is the FIRST thing it failed, not the last.
|
|
58699
58687
|
*/
|
|
58700
|
-
function evaluateStationaryEvidence(row, policy
|
|
58688
|
+
function evaluateStationaryEvidence(row, policy) {
|
|
58701
58689
|
const windowSeen = row.windowSeen + row.prevWindowSeen;
|
|
58702
58690
|
const windowAgreed = row.windowAgreed + row.prevWindowAgreed;
|
|
58703
58691
|
const ratio = windowSeen > 0 ? windowAgreed / windowSeen : 0;
|
|
@@ -58708,11 +58696,6 @@ function evaluateStationaryEvidence(row, policy, classEnabled) {
|
|
|
58708
58696
|
ratio,
|
|
58709
58697
|
spanMs
|
|
58710
58698
|
};
|
|
58711
|
-
if (!classEnabled) return {
|
|
58712
|
-
kind: "accumulating",
|
|
58713
|
-
shortfall: "class-not-promoted",
|
|
58714
|
-
...base
|
|
58715
|
-
};
|
|
58716
58699
|
if (row.agreed < policy.minAgreed) return {
|
|
58717
58700
|
kind: "accumulating",
|
|
58718
58701
|
shortfall: "too-few-agreements",
|
|
@@ -58787,6 +58770,32 @@ function matchEvidenceCandidate(rows, className, bbox, agreeIou) {
|
|
|
58787
58770
|
};
|
|
58788
58771
|
}
|
|
58789
58772
|
/**
|
|
58773
|
+
* Does this observation count as EVIDENCE that the object is parked?
|
|
58774
|
+
*
|
|
58775
|
+
* Two questions, deliberately kept apart:
|
|
58776
|
+
*
|
|
58777
|
+
* - IDENTITY — is this the same object in the same place? That is the IoU,
|
|
58778
|
+
* and it is the only notion of sameness in this module.
|
|
58779
|
+
* - STILLNESS — is it going anywhere? That is the TRACKER's verdict, and
|
|
58780
|
+
* nothing here re-derives it.
|
|
58781
|
+
*
|
|
58782
|
+
* Before this, `agrees` was the IoU alone, so a long run of overlaps WAS the
|
|
58783
|
+
* stillness argument — a second derivation of a fact `StateAnalyzer` already
|
|
58784
|
+
* computes per track, slower and able to disagree with it. Now the ledger adds
|
|
58785
|
+
* only what the analyzer cannot know: that the same still object has been here
|
|
58786
|
+
* across a succession of tracks.
|
|
58787
|
+
*
|
|
58788
|
+
* `entering` is the tracker's undecided window — five frames that begin EVERY
|
|
58789
|
+
* track, including a parked car's re-detection. It is not disagreement, and
|
|
58790
|
+
* counting it as such would punish a candidate for the tracker's own warm-up.
|
|
58791
|
+
* An ABSENT verdict likewise falls back to overlap alone: a path that never had
|
|
58792
|
+
* an opinion must not be silently reclassified.
|
|
58793
|
+
*/
|
|
58794
|
+
function observationAgrees(overlap, agreeIou, movementState) {
|
|
58795
|
+
if (overlap < agreeIou) return false;
|
|
58796
|
+
return movementState !== "moving" && movementState !== "left";
|
|
58797
|
+
}
|
|
58798
|
+
/**
|
|
58790
58799
|
* Fold one observation into a candidate.
|
|
58791
58800
|
*
|
|
58792
58801
|
* `agrees` is decided by the CALLER, because M is what makes the ratio mean
|
|
@@ -59089,7 +59098,7 @@ var StationaryEvidenceLedger = class {
|
|
|
59089
59098
|
* slow or failing store can never change what this frame decides. The
|
|
59090
59099
|
* durable half is fired behind the verdict.
|
|
59091
59100
|
*/
|
|
59092
|
-
observe(observation
|
|
59101
|
+
observe(observation) {
|
|
59093
59102
|
const policy = evidencePolicyFor(observation.className);
|
|
59094
59103
|
this.expire(observation.deviceId, observation.timestamp);
|
|
59095
59104
|
const bindKey = observation.trackId === void 0 ? void 0 : `${observation.deviceId}:${observation.trackId}`;
|
|
@@ -59137,18 +59146,18 @@ var StationaryEvidenceLedger = class {
|
|
|
59137
59146
|
this.capDevice(observation.deviceId);
|
|
59138
59147
|
return {
|
|
59139
59148
|
row,
|
|
59140
|
-
verdict: evaluateStationaryEvidence(row, policy
|
|
59149
|
+
verdict: evaluateStationaryEvidence(row, policy),
|
|
59141
59150
|
promoted: false
|
|
59142
59151
|
};
|
|
59143
59152
|
}
|
|
59144
59153
|
if (bindKey !== void 0) this.binding.set(bindKey, row.key);
|
|
59145
59154
|
}
|
|
59146
|
-
const agrees = overlap
|
|
59155
|
+
const agrees = observationAgrees(overlap, policy.agreeIou, observation.movementState);
|
|
59147
59156
|
const next = foldObservation(row, observation, agrees, overlap);
|
|
59148
59157
|
this.ledger.stage(next);
|
|
59149
59158
|
this.dirty.add(next.key);
|
|
59150
59159
|
if (created) this.capDevice(observation.deviceId);
|
|
59151
|
-
const verdict = evaluateStationaryEvidence(next, policy
|
|
59160
|
+
const verdict = evaluateStationaryEvidence(next, policy);
|
|
59152
59161
|
if (verdict.kind !== "promote") return {
|
|
59153
59162
|
row: next,
|
|
59154
59163
|
verdict,
|
|
@@ -59596,8 +59605,46 @@ var StationaryObjectRegistry = class {
|
|
|
59596
59605
|
});
|
|
59597
59606
|
}
|
|
59598
59607
|
}
|
|
59599
|
-
/**
|
|
59600
|
-
|
|
59608
|
+
/**
|
|
59609
|
+
* Promote a parked track into a persisted stationary entry — or, when the
|
|
59610
|
+
* spot is already censused, CONFIRM the entry sitting on it.
|
|
59611
|
+
*
|
|
59612
|
+
* The occupancy question is asked here and nowhere else, with the same
|
|
59613
|
+
* predicate the per-frame filter uses (`findParkedOccupant`): a box the
|
|
59614
|
+
* filter would suppress as a parked object's own can never become a second
|
|
59615
|
+
* parked object. Until 2026-09-14 this method minted whatever it was handed,
|
|
59616
|
+
* and device 617 reported EIGHT parked vehicles — four of them one car, the
|
|
59617
|
+
* same box to the decimal, promoted again roughly once an hour because the
|
|
59618
|
+
* weak-continuation path (D410) keeps feeding the ledger from a track riding
|
|
59619
|
+
* a parked object. The re-promotion is not the anomaly and is not worth
|
|
59620
|
+
* suppressing: it is a fresh, independent confirmation that the thing is
|
|
59621
|
+
* still there. It just must not produce a neighbour.
|
|
59622
|
+
*
|
|
59623
|
+
* A merge never DOWNGRADES the tenant: it advances the clock and adopts an
|
|
59624
|
+
* identity the tenant lacks (the 617 duplicates arrived unlabelled while the
|
|
59625
|
+
* row they belonged to was already `Car`), and it never overwrites one it
|
|
59626
|
+
* has.
|
|
59627
|
+
*/
|
|
59628
|
+
async promote(entry, config) {
|
|
59629
|
+
const occupant = findParkedOccupant(this.list(entry.deviceId), entry.className, entry.bbox, (config ?? this.matchConfig).suppressIou, (config ?? this.matchConfig).wakeAssocIou);
|
|
59630
|
+
if (occupant !== void 0) {
|
|
59631
|
+
const merged = this.absorbInto(occupant, entry);
|
|
59632
|
+
this.logger.info("stationary promotion absorbed — the spot is already censused", {
|
|
59633
|
+
tags: { deviceId: occupant.deviceId },
|
|
59634
|
+
meta: {
|
|
59635
|
+
deviceId: occupant.deviceId,
|
|
59636
|
+
entryId: occupant.id,
|
|
59637
|
+
className: occupant.className,
|
|
59638
|
+
refusedTrackId: entry.sourceTrackId,
|
|
59639
|
+
parkedForMs: entry.becameStationaryAt - occupant.becameStationaryAt,
|
|
59640
|
+
gainedLabel: occupant.label === void 0 && entry.label !== void 0
|
|
59641
|
+
}
|
|
59642
|
+
});
|
|
59643
|
+
return {
|
|
59644
|
+
outcome: "merged",
|
|
59645
|
+
entry: merged
|
|
59646
|
+
};
|
|
59647
|
+
}
|
|
59601
59648
|
this.indexEntry(entry.deviceId, entry.id);
|
|
59602
59649
|
this.dirty.delete(entry.id);
|
|
59603
59650
|
await this.ledger.put(entry);
|
|
@@ -59606,6 +59653,22 @@ var StationaryObjectRegistry = class {
|
|
|
59606
59653
|
entry,
|
|
59607
59654
|
timestamp: entry.becameStationaryAt
|
|
59608
59655
|
});
|
|
59656
|
+
return {
|
|
59657
|
+
outcome: "born",
|
|
59658
|
+
entry
|
|
59659
|
+
};
|
|
59660
|
+
}
|
|
59661
|
+
/** Fold a refused promotion into the entry that already holds its spot. */
|
|
59662
|
+
absorbInto(occupant, refused) {
|
|
59663
|
+
const merged = {
|
|
59664
|
+
...occupant,
|
|
59665
|
+
lastConfirmedAt: Math.max(occupant.lastConfirmedAt, refused.becameStationaryAt),
|
|
59666
|
+
...occupant.label === void 0 && refused.label !== void 0 ? { label: refused.label } : {},
|
|
59667
|
+
...occupant.keyFrameMediaId === void 0 && refused.keyFrameMediaId !== void 0 ? { keyFrameMediaId: refused.keyFrameMediaId } : {}
|
|
59668
|
+
};
|
|
59669
|
+
this.ledger.stage(merged);
|
|
59670
|
+
this.dirty.add(merged.id);
|
|
59671
|
+
return merged;
|
|
59609
59672
|
}
|
|
59610
59673
|
/**
|
|
59611
59674
|
* Retire entries unconfirmed for longer than the TTL of OBSERVED time, and
|
|
@@ -59693,6 +59756,7 @@ var StationaryObjectRegistry = class {
|
|
|
59693
59756
|
}
|
|
59694
59757
|
});
|
|
59695
59758
|
this.strikeCountersByDevice.clear();
|
|
59759
|
+
for (const deviceId of [...this.idsByDevice.keys()]) this.collapseDuplicates(deviceId);
|
|
59696
59760
|
for (const id of [...this.dirty]) {
|
|
59697
59761
|
this.dirty.delete(id);
|
|
59698
59762
|
const entry = this.ledger.get(id);
|
|
@@ -59834,6 +59898,46 @@ var StationaryObjectRegistry = class {
|
|
|
59834
59898
|
/** Retire one entry: index, staged advance, pending strike and durable row
|
|
59835
59899
|
* all go. A strike that outlived its entry would arm the NEXT entry to reuse
|
|
59836
59900
|
* the id — the registry mints UUIDs, but the invariant is cheap to keep. */
|
|
59901
|
+
/**
|
|
59902
|
+
* Fold every row that holds another row's spot into the OLDEST of them.
|
|
59903
|
+
*
|
|
59904
|
+
* The survivor is the oldest because it is the one carrying the object's real
|
|
59905
|
+
* `becameStationaryAt` — "parked since 14:30" is the fact the operator reads,
|
|
59906
|
+
* and the duplicates are all later re-discoveries of the same thing. It
|
|
59907
|
+
* inherits any identity the losers carry and that it lacks, and the losers'
|
|
59908
|
+
* rows are deleted outright: a retirement would announce a departure that
|
|
59909
|
+
* never happened.
|
|
59910
|
+
*
|
|
59911
|
+
* Uses the same predicate as the filter and the birth guard, so a collapse
|
|
59912
|
+
* can only ever remove a row that could not have been independently confirmed
|
|
59913
|
+
* anyway.
|
|
59914
|
+
*/
|
|
59915
|
+
collapseDuplicates(deviceId) {
|
|
59916
|
+
const entries = [...this.list(deviceId)].sort((a, b) => a.becameStationaryAt - b.becameStationaryAt);
|
|
59917
|
+
if (entries.length < 2) return;
|
|
59918
|
+
const survivors = [];
|
|
59919
|
+
for (const candidate of entries) {
|
|
59920
|
+
const occupant = findParkedOccupant(survivors, candidate.className, candidate.bbox, this.matchConfig.suppressIou, this.matchConfig.wakeAssocIou);
|
|
59921
|
+
if (occupant === void 0) {
|
|
59922
|
+
survivors.push(candidate);
|
|
59923
|
+
continue;
|
|
59924
|
+
}
|
|
59925
|
+
const merged = this.absorbInto(occupant, candidate);
|
|
59926
|
+
survivors[survivors.indexOf(occupant)] = merged;
|
|
59927
|
+
this.dropEntry(deviceId, candidate.id);
|
|
59928
|
+
this.logger.info("stationary duplicate collapsed — two rows, one spot", {
|
|
59929
|
+
tags: { deviceId },
|
|
59930
|
+
meta: {
|
|
59931
|
+
deviceId,
|
|
59932
|
+
keptEntryId: occupant.id,
|
|
59933
|
+
droppedEntryId: candidate.id,
|
|
59934
|
+
className: candidate.className,
|
|
59935
|
+
keptSinceMs: occupant.becameStationaryAt,
|
|
59936
|
+
droppedSinceMs: candidate.becameStationaryAt
|
|
59937
|
+
}
|
|
59938
|
+
});
|
|
59939
|
+
}
|
|
59940
|
+
}
|
|
59837
59941
|
dropEntry(deviceId, id) {
|
|
59838
59942
|
this.idsByDevice.get(deviceId)?.delete(id);
|
|
59839
59943
|
this.dirty.delete(id);
|
|
@@ -69876,18 +69980,6 @@ var StationarySettingsSchema = object({
|
|
|
69876
69980
|
* session open (also the minimum track age). Creates nothing — birth is
|
|
69877
69981
|
* decided by repeated evidence. `SESSION_STILLNESS_WINDOW_MS`. */
|
|
69878
69982
|
sessionStillnessWindowMs: number().int().min(5e3).max(12e4).default(SESSION_STILLNESS_WINDOW_MS),
|
|
69879
|
-
/**
|
|
69880
|
-
* Let a motionless PERSON become a stationary entry on this camera.
|
|
69881
|
-
*
|
|
69882
|
-
* Off, and it is a measurement rather than caution. Replaying 24 h of person
|
|
69883
|
-
* observations from devices 617/1443/590/592 through the evidence ledger
|
|
69884
|
-
* produces 6-22 entries a day depending on how strictly it is tuned, and none
|
|
69885
|
-
* of them can be tuned to zero: device 592 alone yielded eleven candidates at
|
|
69886
|
-
* a perfect agreement ratio between 02:00 and 05:00. A stationary entry
|
|
69887
|
-
* SUPPRESSES its object, so a wrong one makes the camera blind to a person —
|
|
69888
|
-
* turn this on only for a camera where you know what is standing there.
|
|
69889
|
-
*/
|
|
69890
|
-
promotePersons: boolean().default(false),
|
|
69891
69983
|
/** Observed-time TTL: retire an entry unconfirmed for this long (measured on
|
|
69892
69984
|
* frames-flowing time, not wall-clock). `ENTRY_TTL_MS`. */
|
|
69893
69985
|
entryTtlMs: number().int().min(6e4).max(18e5).default(ENTRY_TTL_MS)
|
|
@@ -69907,7 +69999,6 @@ function resolveStationarySettings(raw) {
|
|
|
69907
69999
|
suppressIou: pick("suppressIou"),
|
|
69908
70000
|
wakeAssocIou: pick("wakeAssocIou"),
|
|
69909
70001
|
sessionStillnessWindowMs: pick("sessionStillnessWindowMs"),
|
|
69910
|
-
promotePersons: pick("promotePersons"),
|
|
69911
70002
|
entryTtlMs: pick("entryTtlMs")
|
|
69912
70003
|
};
|
|
69913
70004
|
}
|
|
@@ -70443,13 +70534,6 @@ function buildDetectionSettingsSections() {
|
|
|
70443
70534
|
unit: "s",
|
|
70444
70535
|
displayScale: 1e3
|
|
70445
70536
|
},
|
|
70446
|
-
{
|
|
70447
|
-
type: "boolean",
|
|
70448
|
-
key: "promotePersons",
|
|
70449
|
-
label: "Also park motionless people",
|
|
70450
|
-
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.",
|
|
70451
|
-
default: STATIONARY_DEFAULTS.promotePersons
|
|
70452
|
-
},
|
|
70453
70537
|
{
|
|
70454
70538
|
type: "slider",
|
|
70455
70539
|
key: "entryTtlMs",
|
|
@@ -73793,11 +73877,7 @@ var ZoneAnalyticsProvider = class {
|
|
|
73793
73877
|
});
|
|
73794
73878
|
}
|
|
73795
73879
|
this.snapshots.set(input.deviceId, snapshot);
|
|
73796
|
-
|
|
73797
|
-
if (noteOccupancy !== void 0) {
|
|
73798
|
-
noteOccupancy(input.deviceId, total, input.timestamp);
|
|
73799
|
-
for (const zone of snapshot.zones) noteOccupancy(input.deviceId, zone.totalObjects, input.timestamp, zone.zoneId);
|
|
73800
|
-
}
|
|
73880
|
+
this.noteLtsSamples(input.deviceId, snapshot, input.timestamp);
|
|
73801
73881
|
this.appendHistory(input.deviceId, snapshot);
|
|
73802
73882
|
this.emitSnapshot(input.deviceId, snapshot);
|
|
73803
73883
|
this.sliceThrottle.push(input.deviceId, snapshot);
|
|
@@ -73864,11 +73944,30 @@ var ZoneAnalyticsProvider = class {
|
|
|
73864
73944
|
});
|
|
73865
73945
|
if (snapshot) {
|
|
73866
73946
|
this.appendHistory(deviceId, snapshot);
|
|
73947
|
+
this.noteLtsSamples(deviceId, snapshot, now);
|
|
73867
73948
|
this.emitSnapshot(deviceId, snapshot);
|
|
73868
73949
|
this.sliceThrottle.push(deviceId, snapshot);
|
|
73869
73950
|
}
|
|
73870
73951
|
}
|
|
73871
73952
|
}
|
|
73953
|
+
/**
|
|
73954
|
+
* Append this snapshot to the long-term occupancy series — frame-wide first
|
|
73955
|
+
* (`scope: ''`), then one sample per zone.
|
|
73956
|
+
*
|
|
73957
|
+
* Shared by BOTH producers on purpose. It used to live inline in
|
|
73958
|
+
* `recordFrame`, so the baseline sampler — the only producer a camera with no
|
|
73959
|
+
* frames flowing has — wrote the RAM ring, the NC watcher and the state slice
|
|
73960
|
+
* but never LTS. The long-term series therefore had a hole exactly where the
|
|
73961
|
+
* scene was nothing but parked objects: the one span an occupancy history is
|
|
73962
|
+
* asked about. The RAM ring showed a flat line over the same minutes, which
|
|
73963
|
+
* is what made the gap invisible.
|
|
73964
|
+
*/
|
|
73965
|
+
noteLtsSamples(deviceId, snapshot, timestamp) {
|
|
73966
|
+
const note = this.ctx.onOccupancySample;
|
|
73967
|
+
if (note === void 0) return;
|
|
73968
|
+
note(deviceId, snapshot.frame.totalObjects, timestamp);
|
|
73969
|
+
for (const zone of snapshot.zones) note(deviceId, zone.totalObjects, timestamp, zone.zoneId);
|
|
73970
|
+
}
|
|
73872
73971
|
/** Push one snapshot to the occupancy tap, isolating any consumer throw from
|
|
73873
73972
|
* the frame path (occupancy notifications must never break analytics). */
|
|
73874
73973
|
emitSnapshot(deviceId, snapshot) {
|
|
@@ -78203,7 +78302,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78203
78302
|
}
|
|
78204
78303
|
});
|
|
78205
78304
|
}
|
|
78206
|
-
await this.processFrame(deviceId, frame, data.source ?? "pipeline", data.frameHandle, data.detailSteps);
|
|
78305
|
+
await this.processFrame(deviceId, frame, data.source ?? "pipeline", data.frameHandle, data.detailSteps, data.occasion ?? "session");
|
|
78207
78306
|
}
|
|
78208
78307
|
/**
|
|
78209
78308
|
* Run one detection frame through the analysis layers for a given
|
|
@@ -78213,7 +78312,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78213
78312
|
* tracking/zone/event state never crosses between sources. Emits the
|
|
78214
78313
|
* SAME canonical events, distinguished only by `source`.
|
|
78215
78314
|
*/
|
|
78216
|
-
async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps) {
|
|
78315
|
+
async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps, occasion = "session") {
|
|
78217
78316
|
if (this.shuttingDown) return;
|
|
78218
78317
|
if (!await this.bindingCache.isActive(deviceId)) {
|
|
78219
78318
|
const now = Date.now();
|
|
@@ -78621,7 +78720,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78621
78720
|
const evidence = this.stationaryEvidence;
|
|
78622
78721
|
for (const t of result.tracked) {
|
|
78623
78722
|
if (packageClasses.has(t.className)) continue;
|
|
78624
|
-
const classEnabled = t.className === "person" ? stationarySettings.promotePersons : evidencePolicyFor(t.className).promotesByDefault;
|
|
78625
78723
|
const active = this.trackStore?.peekActive(t.trackId) ?? null;
|
|
78626
78724
|
const observedLabel = active === null ? void 0 : displayLabel(active);
|
|
78627
78725
|
const observedKeyFrame = this.residents.keyFrameMediaId(t.trackId);
|
|
@@ -78630,12 +78728,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78630
78728
|
className: t.className,
|
|
78631
78729
|
bbox: { ...t.bbox },
|
|
78632
78730
|
timestamp: result.timestamp,
|
|
78633
|
-
source:
|
|
78731
|
+
source: occasion,
|
|
78634
78732
|
trackId: t.trackId,
|
|
78635
78733
|
...observedKeyFrame !== void 0 ? { keyFrameMediaId: observedKeyFrame } : {},
|
|
78636
|
-
...observedLabel !== void 0 ? { label: observedLabel } : {}
|
|
78734
|
+
...observedLabel !== void 0 ? { label: observedLabel } : {},
|
|
78735
|
+
movementState: t.state
|
|
78637
78736
|
};
|
|
78638
|
-
const outcome = evidence.observe(observation
|
|
78737
|
+
const outcome = evidence.observe(observation);
|
|
78639
78738
|
if (outcome === void 0 || !outcome.promoted) continue;
|
|
78640
78739
|
this.birthStationaryFromEvidence({
|
|
78641
78740
|
deviceId,
|
|
@@ -81738,7 +81837,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
81738
81837
|
...label !== void 0 ? { label } : {},
|
|
81739
81838
|
...keyFrameMediaId !== void 0 ? { keyFrameMediaId } : {}
|
|
81740
81839
|
};
|
|
81741
|
-
if (plausible)
|
|
81840
|
+
if (plausible) {
|
|
81841
|
+
const gate = this.stationarySettingsFromCache(deviceId);
|
|
81842
|
+
this.stationaryRegistry?.promote(entry, {
|
|
81843
|
+
suppressIou: gate.suppressIou,
|
|
81844
|
+
wakeAssocIou: gate.wakeAssocIou
|
|
81845
|
+
});
|
|
81846
|
+
}
|
|
81742
81847
|
processor.dropTrack(track.trackId);
|
|
81743
81848
|
this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
|
|
81744
81849
|
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DqVUwKib.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.244",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|