@camstack/addon-post-analysis 1.2.269 → 1.2.271
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-B78I61U1.js → dist-DTeAk1Ce.js} +101 -7
- package/dist/{dist-ABnG0S3f.mjs → dist-K6zDULiD.mjs} +101 -7
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D42gEB0t.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bs8m-J-O.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C8ZDqFq3.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B0sS9pCe.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-fxXc6Mk3.mjs → hostInit-jMBJBSqj.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +395 -77
- package/dist/pipeline-analytics/index.mjs +395 -77
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/python/raw_tensor_inference.py +16 -1
- package/python/session_threads.py +33 -0
- package/python/test_session_threads.py +65 -0
- package/python/text_encoder_inference.py +15 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as TimelapseRuleSchema, $t as array, A as NcConditionDescriptorSchema, At as readDeviceStateFrom, B as NcTaxonomySchema, Bt as zoneAnalyticsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as mayWriteToLocation, D as NC_DEFAULT_SNOOZE_MINUTES, Dt as pickClusterStepModels, E as NC_CONDITION_CATALOG, F as NcScheduleSchema, Ft as storageOccupancyCapability, Gt as createEvent, Ht as BaseAddon, I as NcSnoozeInputSchema, It as subKindsOf, J as SCENE_DIVERGED, Jt as hydrateSchema, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as customAction, L as NcSnoozeSchema, Lt as systemEventFilterApplies, M as NcRulePatchSchema, Mt as resolveLocationMode, N as NcRuleSchema, Ot as pipelineAnalyticsCapability, P as NcRuleTargetSchema, Pt as sceneMonitorCapability, Q as TimelapseRulePatchSchema, Qt as _enum, R as NcSnoozeSuppressedSchema, Rt as vectorDimFromBase64, St as kebabToCamel, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as occupancyScope, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as CamProfileSchema, V as OpsLogEntrySchema, Vt as errMsg$1, W as RetrainStatusSchema, Wt as DeviceType, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as nodePin, Y as SceneMonitorSchema, Yt as isDeviceScopedCap, Z as TimelapseRuleInputSchema, Zt as sleep$1, _ as FULL_IMAGE_BBOX, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, an as partialRecord, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isScheduleActive, cn as unknown, ct as cosineSimilarity$1, dt as encodeVectorBase64, en as boolean, 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 object, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readTimelapseGeneratedAt, k as NC_TAXONOMY, kt as plateGalleryCapability, ln as EventCategory, lt as deriveRecordingMode, m as EVENT_OWNER_TYPES, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as literal, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as record, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as defineCustomActions, r as BaseDevice, rn as number, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as string, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as discriminatedUnion, 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 videoclipsCapability } from "../dist-
|
|
1
|
+
import { $ as TimelapseRuleSchema, $t as array, A as NcConditionDescriptorSchema, At as readDeviceStateFrom, B as NcTaxonomySchema, Bt as zoneAnalyticsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as mayWriteToLocation, D as NC_DEFAULT_SNOOZE_MINUTES, Dt as pickClusterStepModels, E as NC_CONDITION_CATALOG, F as NcScheduleSchema, Ft as storageOccupancyCapability, Gt as createEvent, Ht as BaseAddon, I as NcSnoozeInputSchema, It as subKindsOf, J as SCENE_DIVERGED, Jt as hydrateSchema, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as customAction, L as NcSnoozeSchema, Lt as systemEventFilterApplies, M as NcRulePatchSchema, Mt as resolveLocationMode, N as NcRuleSchema, Ot as pipelineAnalyticsCapability, P as NcRuleTargetSchema, Pt as sceneMonitorCapability, Q as TimelapseRulePatchSchema, Qt as _enum, R as NcSnoozeSuppressedSchema, Rt as vectorDimFromBase64, St as kebabToCamel, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as occupancyScope, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as CamProfileSchema, V as OpsLogEntrySchema, Vt as errMsg$1, W as RetrainStatusSchema, Wt as DeviceType, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as nodePin, Y as SceneMonitorSchema, Yt as isDeviceScopedCap, Z as TimelapseRuleInputSchema, Zt as sleep$1, _ as FULL_IMAGE_BBOX, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, an as partialRecord, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isScheduleActive, cn as unknown, ct as cosineSimilarity$1, dt as encodeVectorBase64, en as boolean, 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 object, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readTimelapseGeneratedAt, k as NC_TAXONOMY, kt as plateGalleryCapability, ln as EventCategory, lt as deriveRecordingMode, m as EVENT_OWNER_TYPES, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as literal, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as record, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as defineCustomActions, r as BaseDevice, rn as number, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as string, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as discriminatedUnion, 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 videoclipsCapability } from "../dist-K6zDULiD.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";
|
|
@@ -30033,10 +30033,31 @@ var MediaStore = class {
|
|
|
30033
30033
|
* was simply never returned, which is why every retention ops-log row this
|
|
30034
30034
|
* addon has ever written says `bytesReclaimed: 0` (D161).
|
|
30035
30035
|
*/
|
|
30036
|
-
async deleteForOwnerCounted(ownerKind, ownerIds) {
|
|
30037
|
-
return this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds));
|
|
30036
|
+
async deleteForOwnerCounted(ownerKind, ownerIds, spare) {
|
|
30037
|
+
return this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds, spare));
|
|
30038
30038
|
}
|
|
30039
|
-
|
|
30039
|
+
/**
|
|
30040
|
+
* Who owns a media row — kind and id — or `null` for an unknown id. The
|
|
30041
|
+
* stationary registry asks this before releasing an entry's key frame
|
|
30042
|
+
* (D547): the row is still `track`-owned by a track whose row is gone, and
|
|
30043
|
+
* only when no track row owns it is the entry the last thing vouching for it.
|
|
30044
|
+
*/
|
|
30045
|
+
async ownerOf(mediaId) {
|
|
30046
|
+
const row = await this.store.get.query({
|
|
30047
|
+
collection: MEDIA_COLLECTION,
|
|
30048
|
+
key: String(mediaId)
|
|
30049
|
+
});
|
|
30050
|
+
if (!row) return null;
|
|
30051
|
+
const data = row;
|
|
30052
|
+
const ownerKind = String(data["ownerKind"] ?? "");
|
|
30053
|
+
const ownerId = String(data["ownerId"] ?? "");
|
|
30054
|
+
if (!isMediaOwnerKind(ownerKind) || ownerId === "") return null;
|
|
30055
|
+
return {
|
|
30056
|
+
ownerKind,
|
|
30057
|
+
ownerId
|
|
30058
|
+
};
|
|
30059
|
+
}
|
|
30060
|
+
async deleteForOwnerUnchecked(ownerKind, ownerIds, spare) {
|
|
30040
30061
|
let rows = 0;
|
|
30041
30062
|
let bytes = 0;
|
|
30042
30063
|
for (const ownerId of ownerIds) {
|
|
@@ -30048,6 +30069,7 @@ var MediaStore = class {
|
|
|
30048
30069
|
} }
|
|
30049
30070
|
});
|
|
30050
30071
|
for (const row of found) {
|
|
30072
|
+
if (spare !== void 0 && spare.has(Number(row.id))) continue;
|
|
30051
30073
|
const rowData = row.data;
|
|
30052
30074
|
const path = String(rowData["path"] ?? "");
|
|
30053
30075
|
const sizeBytes = Number(rowData["sizeBytes"]);
|
|
@@ -30366,16 +30388,17 @@ var MediaStore = class {
|
|
|
30366
30388
|
* `deleteForOwner` is best-effort per row, so one bad row can't abort the
|
|
30367
30389
|
* batch.
|
|
30368
30390
|
*/
|
|
30369
|
-
async deleteByTracks(trackIds) {
|
|
30391
|
+
async deleteByTracks(trackIds, opts) {
|
|
30392
|
+
const spare = opts?.spare;
|
|
30370
30393
|
const own = await this.gated(async () => {
|
|
30371
|
-
const tracks = await this.deleteForOwnerUnchecked("track", trackIds);
|
|
30372
|
-
const faces = await this.deleteForOwnerUnchecked("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
|
|
30394
|
+
const tracks = await this.deleteForOwnerUnchecked("track", trackIds, spare);
|
|
30395
|
+
const faces = await this.deleteForOwnerUnchecked("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id), spare);
|
|
30373
30396
|
return {
|
|
30374
30397
|
rows: tracks.rows + faces.rows,
|
|
30375
30398
|
bytes: tracks.bytes + faces.bytes
|
|
30376
30399
|
};
|
|
30377
30400
|
});
|
|
30378
|
-
const plates = await this.deleteForOwnerCounted("plate", trackIds.map((id) => PLATE_MEDIA_OWNER_PREFIX + id));
|
|
30401
|
+
const plates = await this.deleteForOwnerCounted("plate", trackIds.map((id) => PLATE_MEDIA_OWNER_PREFIX + id), spare);
|
|
30379
30402
|
return [{
|
|
30380
30403
|
family: "media",
|
|
30381
30404
|
rows: own.rows + plates.rows,
|
|
@@ -37154,6 +37177,14 @@ var SpawnRefusalSampler = class {
|
|
|
37154
37177
|
return count % 10 === 0 ? count : void 0;
|
|
37155
37178
|
}
|
|
37156
37179
|
};
|
|
37180
|
+
function shouldFreezePrediction(input) {
|
|
37181
|
+
const { speedPxPerMs, emitIntervalMs, boxDiagonalPx, stationarySpeedPx } = input;
|
|
37182
|
+
const calibratedBar = input.stillSpeedFracPerSec;
|
|
37183
|
+
const optedIn = stationarySpeedPx === 0;
|
|
37184
|
+
if (optedIn && calibratedBar !== void 0 && calibratedBar > 0 && boxDiagonalPx > 0) return speedPxPerMs * 1e3 / boxDiagonalPx < calibratedBar;
|
|
37185
|
+
const bar = optedIn ? 2 : stationarySpeedPx;
|
|
37186
|
+
return speedPxPerMs * emitIntervalMs < bar;
|
|
37187
|
+
}
|
|
37157
37188
|
//#endregion
|
|
37158
37189
|
//#region src/pipeline-analytics/pipeline/tracker/sort-tracker.ts
|
|
37159
37190
|
var DEFAULT_TRACKER_CONFIG = {
|
|
@@ -37749,9 +37780,10 @@ var SortTracker = class SortTracker {
|
|
|
37749
37780
|
* Where a track is expected at `nowMs` — extrapolated by velocity while
|
|
37750
37781
|
* coasting (`predictiveCoasting`), else its last known bbox.
|
|
37751
37782
|
*
|
|
37752
|
-
* A stationary track
|
|
37753
|
-
*
|
|
37754
|
-
*
|
|
37783
|
+
* A stationary track is frozen so bbox jitter cannot walk the box off a
|
|
37784
|
+
* sitting object. WHICH bar decides — the legacy px-per-update one or this
|
|
37785
|
+
* camera's calibrated diagonals-per-second — is `shouldFreezePrediction`'s
|
|
37786
|
+
* question, not this method's. Otherwise the drift is the
|
|
37755
37787
|
* velocity carried over the ELAPSED interval, capped by
|
|
37756
37788
|
* {@link driftAllowancePx} — which is itself a function of that interval, so
|
|
37757
37789
|
* a stale velocity cannot fling the box across the frame and a long, honest
|
|
@@ -37760,7 +37792,13 @@ var SortTracker = class SortTracker {
|
|
|
37760
37792
|
*/
|
|
37761
37793
|
predicted(t, nowMs) {
|
|
37762
37794
|
if (!this.config.predictiveCoasting) return t.bbox;
|
|
37763
|
-
if (
|
|
37795
|
+
if (shouldFreezePrediction({
|
|
37796
|
+
speedPxPerMs: Math.hypot(t.velocity.dx, t.velocity.dy),
|
|
37797
|
+
emitIntervalMs: t.emitIntervalMs,
|
|
37798
|
+
boxDiagonalPx: Math.hypot(t.bbox.w, t.bbox.h),
|
|
37799
|
+
stationarySpeedPx: this.config.stationarySpeedPx,
|
|
37800
|
+
...this.config.stillSpeedFracPerSec !== void 0 ? { stillSpeedFracPerSec: this.config.stillSpeedFracPerSec } : {}
|
|
37801
|
+
})) return t.bbox;
|
|
37764
37802
|
if (!this.config.reacquireTimeScaled) {
|
|
37765
37803
|
const f = t.age + 1;
|
|
37766
37804
|
const perFrameX = t.velocity.dx * t.emitIntervalMs;
|
|
@@ -39346,7 +39384,9 @@ var FrameProcessor = class {
|
|
|
39346
39384
|
const refusals = [];
|
|
39347
39385
|
const labelPatchByTrack = /* @__PURE__ */ new Map();
|
|
39348
39386
|
const tracked = trackedDetections.map((td) => {
|
|
39349
|
-
const
|
|
39387
|
+
const objectState = objectStates.find((o) => o.trackId === td.trackId);
|
|
39388
|
+
const state = mapObjectStateToTrackState(objectState?.state);
|
|
39389
|
+
const settling = objectState?.stationarySince !== void 0 && state !== "idle";
|
|
39350
39390
|
const candidate = resolveDetectionLabelCandidate({
|
|
39351
39391
|
className: td.class,
|
|
39352
39392
|
originalClass: td.originalClass,
|
|
@@ -39384,6 +39424,7 @@ var FrameProcessor = class {
|
|
|
39384
39424
|
zones: zonesByTrack.get(td.trackId) ?? [],
|
|
39385
39425
|
state,
|
|
39386
39426
|
matchedThisFrame: td.matchedThisFrame !== false,
|
|
39427
|
+
settling,
|
|
39387
39428
|
...td.resurrectionScene !== void 0 ? { resurrectionScene: td.resurrectionScene } : {},
|
|
39388
39429
|
...labelPatch !== void 0 ? { labelPatch } : {},
|
|
39389
39430
|
...emb !== void 0 ? {
|
|
@@ -40379,8 +40420,16 @@ var TrackCloser = class {
|
|
|
40379
40420
|
try {
|
|
40380
40421
|
const trackStore = this.deps.trackStore();
|
|
40381
40422
|
if (!trackStore) return;
|
|
40382
|
-
const
|
|
40423
|
+
const discarded = [];
|
|
40424
|
+
const discardTracks = this.deps.discardTracks;
|
|
40425
|
+
const expired = await trackStore.expireStale(this.now(), {
|
|
40426
|
+
...this.deps.isKnownParked !== void 0 ? { isKnownParked: this.deps.isKnownParked } : {},
|
|
40427
|
+
...discardTracks !== void 0 ? { onDiscard: (t) => {
|
|
40428
|
+
discarded.push(t);
|
|
40429
|
+
} } : {}
|
|
40430
|
+
});
|
|
40383
40431
|
for (const t of expired) await this.closeExpired(t);
|
|
40432
|
+
if (discardTracks !== void 0 && discarded.length > 0) await discardTracks(discarded);
|
|
40384
40433
|
} catch (err) {
|
|
40385
40434
|
if (this.deps.isShuttingDown()) return;
|
|
40386
40435
|
this.deps.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
@@ -44078,6 +44127,19 @@ var VEHICLE_CLASSES$1 = new Set([
|
|
|
44078
44127
|
"truck",
|
|
44079
44128
|
"bus"
|
|
44080
44129
|
]);
|
|
44130
|
+
/**
|
|
44131
|
+
* A short motionless PERSON is stamped and persisted, never discarded (D547).
|
|
44132
|
+
*
|
|
44133
|
+
* The discard exists for re-sightings of an OBJECT an entity represents — a
|
|
44134
|
+
* parked car the registry censuses in two sessions, a plate flash on it. No
|
|
44135
|
+
* entity ever represents a person who stood still for ten seconds (the person
|
|
44136
|
+
* policy needs thirty minutes of agreement, on purpose), and the face read
|
|
44137
|
+
* such a track holds is the only record of who was there. Deleting the track
|
|
44138
|
+
* would delete that; hiding the row from the feed was always the whole rule.
|
|
44139
|
+
*/
|
|
44140
|
+
function isDiscardExemptClass(className) {
|
|
44141
|
+
return className.toLowerCase() === "person";
|
|
44142
|
+
}
|
|
44081
44143
|
function motionlessMaxPxForClass(className) {
|
|
44082
44144
|
return VEHICLE_CLASSES$1.has(className.toLowerCase()) ? 8 : 40;
|
|
44083
44145
|
}
|
|
@@ -44835,7 +44897,30 @@ var TrackStore = class {
|
|
|
44835
44897
|
for (const [trackId, t] of this.active) {
|
|
44836
44898
|
if (nowMs - t.lastSeen < this.config.ttlMs) continue;
|
|
44837
44899
|
t.active = false;
|
|
44838
|
-
|
|
44900
|
+
const knownParked = t.positions.length > 0 && opts?.isKnownParked?.(t) === true;
|
|
44901
|
+
const shortMotionless = t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions), t.className);
|
|
44902
|
+
const stillObject = knownParked || shortMotionless;
|
|
44903
|
+
const discardable = knownParked || shortMotionless && !isDiscardExemptClass(t.className);
|
|
44904
|
+
const onDiscard = opts?.onDiscard;
|
|
44905
|
+
if (discardable && onDiscard !== void 0) {
|
|
44906
|
+
const record = cloneTrack(t);
|
|
44907
|
+
this.active.delete(trackId);
|
|
44908
|
+
this.noteRowClosed(trackId, nowMs);
|
|
44909
|
+
this.logger.debug("track discarded on expiry — a still object is an entity, not a track", {
|
|
44910
|
+
tags: { deviceId: t.deviceId },
|
|
44911
|
+
meta: {
|
|
44912
|
+
deviceId: t.deviceId,
|
|
44913
|
+
trackId,
|
|
44914
|
+
className: t.className,
|
|
44915
|
+
lifeMs: t.lastSeen - t.firstSeen,
|
|
44916
|
+
netDisplacementPx: Math.round(netDisplacementPx(t.positions)),
|
|
44917
|
+
knownParked
|
|
44918
|
+
}
|
|
44919
|
+
});
|
|
44920
|
+
onDiscard(record);
|
|
44921
|
+
continue;
|
|
44922
|
+
}
|
|
44923
|
+
if (stillObject) t.closedReason = "stationary-promotion";
|
|
44839
44924
|
const record = cloneTrack(t);
|
|
44840
44925
|
try {
|
|
44841
44926
|
await this.persistCompleted(record);
|
|
@@ -53396,6 +53481,7 @@ var FaceRecognizer = class {
|
|
|
53396
53481
|
zones: [],
|
|
53397
53482
|
state: "moving",
|
|
53398
53483
|
matchedThisFrame: true,
|
|
53484
|
+
settling: false,
|
|
53399
53485
|
embedding: input.embedding,
|
|
53400
53486
|
embeddingModelId: modelId,
|
|
53401
53487
|
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
@@ -60790,64 +60876,53 @@ function stillnessVerdictFor(source, trackState) {
|
|
|
60790
60876
|
return source === "burst" ? void 0 : trackState;
|
|
60791
60877
|
}
|
|
60792
60878
|
/**
|
|
60793
|
-
*
|
|
60794
|
-
*
|
|
60795
|
-
* Two questions, deliberately kept apart:
|
|
60879
|
+
* What one observation is worth to a candidate: `agree` (N and M), `disagree`
|
|
60880
|
+
* (M only) or `undecided` (neither).
|
|
60796
60881
|
*
|
|
60797
|
-
*
|
|
60798
|
-
* and it is the only notion of sameness in this module.
|
|
60799
|
-
* - STILLNESS — is it going anywhere? On a session frame that is the
|
|
60800
|
-
* TRACKER's verdict, and nothing here re-derives it. On a burst frame it is
|
|
60801
|
-
* the OCCASION: the runner rechecks a still scene and nothing else (see
|
|
60802
|
-
* {@link stillnessVerdictFor}).
|
|
60882
|
+
* ## Why a third answer exists (D547)
|
|
60803
60883
|
*
|
|
60804
|
-
*
|
|
60884
|
+
* The ratio N/M exists to dilute a car that merely drove through the spot.
|
|
60885
|
+
* It was also diluting a car that had not yet had the CHANCE to be called
|
|
60886
|
+
* still: the five `entered` frames every track begins with, and every frame
|
|
60887
|
+
* on which the analyzer's settled test already passed but
|
|
60888
|
+
* `stationaryThresholdSec` had not elapsed — `moving` in name only. On a
|
|
60889
|
+
* session-dispatched camera those frames are a third of every 36 s track, and
|
|
60890
|
+
* a session cut short by a passer-by is made of nothing else. Measured on
|
|
60891
|
+
* device 617 (DNRG309, 2026-09-19 20:32 → 09-20 07:53): 13 of the 21 tracks
|
|
60892
|
+
* bound to the candidate never reached `idle`, the ledger's ratio was 0.493
|
|
60893
|
+
* against 0.6, and a parked car took 11.3 h and 19 tracks to be censused.
|
|
60805
60894
|
*
|
|
60806
|
-
*
|
|
60807
|
-
*
|
|
60808
|
-
* the
|
|
60809
|
-
*
|
|
60810
|
-
* living 1 to 6 seconds, identical boxes under different ids, which tipped the
|
|
60811
|
-
* ledger into an entry on 10 agreements (the real cars carried 773 and 1565).
|
|
60812
|
-
* The first answer was a second clock in this module (`MIN_STILL_FOR_EVIDENCE_MS`,
|
|
60813
|
-
* 10 s) — a copy of the tracker's own dwell, kept in step by hand. The
|
|
60814
|
-
* shortcut is gone instead: `idle` now means the per-device
|
|
60815
|
-
* `stationaryThresholdSec` has elapsed, on every camera, and this module has
|
|
60816
|
-
* no number of its own.
|
|
60817
|
-
*
|
|
60818
|
-
* ## A verdict, not the absence of one
|
|
60819
|
-
*
|
|
60820
|
-
* `entered` — the five-frame warm-up every track begins with — does not agree
|
|
60821
|
-
* on a session frame. Measured on 617 over 12 h (2026-09-15): a car merely
|
|
60822
|
-
* passing the edge is gone in ~8 frames, five of them `entered`, and 5/8 = 0.62
|
|
60823
|
-
* cleared the 0.6 ratio unaided — an entry born on an empty spot, retired 24
|
|
60824
|
-
* absent readings later, one "parked" and one "departed" notification for a
|
|
60825
|
-
* passage. A parked car's re-detection loses five observations of ~280.
|
|
60826
|
-
*
|
|
60827
|
-
* A session observation with NO verdict at all does not agree either. There
|
|
60828
|
-
* is no trackless producer any more (the hydrated baseline sampler is gone),
|
|
60829
|
-
* so an absent verdict on a session frame is a producer that forgot the
|
|
60830
|
-
* field — the one way D494 said this could rot — and it must read as a miss,
|
|
60831
|
-
* never as the old geometry-only rule.
|
|
60895
|
+
* "Not yet decided" is not evidence either way, so it counts as neither. A
|
|
60896
|
+
* subject that is TRAVELLING — `moving` with the settled test failing — still
|
|
60897
|
+
* dilutes, so the drive-through case D508 measured (5 `entered` of 8 frames)
|
|
60898
|
+
* now scores 0/3 instead of the 5/8 that once minted an entry on an empty spot.
|
|
60832
60899
|
*/
|
|
60833
|
-
function
|
|
60834
|
-
if (overlap < agreeIou) return
|
|
60835
|
-
if (source === "burst") return
|
|
60836
|
-
|
|
60900
|
+
function observationVote(overlap, agreeIou, source, movementState, settling) {
|
|
60901
|
+
if (overlap < agreeIou) return "disagree";
|
|
60902
|
+
if (source === "burst") return "agree";
|
|
60903
|
+
if (movementState === "idle") return "agree";
|
|
60904
|
+
if (movementState === "entered") return "undecided";
|
|
60905
|
+
if (movementState === "moving" && settling) return "undecided";
|
|
60906
|
+
return "disagree";
|
|
60837
60907
|
}
|
|
60838
60908
|
/**
|
|
60839
60909
|
* Fold one observation into a candidate.
|
|
60840
60910
|
*
|
|
60841
|
-
* `
|
|
60911
|
+
* The `vote` is decided by the CALLER, because M is what makes the ratio mean
|
|
60842
60912
|
* anything: once a track has bound to a candidate, every later observation of
|
|
60843
60913
|
* that track counts toward M whether it agrees or not. That is what dilutes a
|
|
60844
60914
|
* car that merely drove through the spot, and it is why this function cannot
|
|
60845
60915
|
* re-derive agreement from the box alone.
|
|
60846
60916
|
*/
|
|
60847
|
-
function foldObservation(row, observation,
|
|
60848
|
-
const seen = row.seen + 1;
|
|
60917
|
+
function foldObservation(row, observation, vote, overlap) {
|
|
60849
60918
|
const rotated = rotateWindow(row, observation.timestamp);
|
|
60850
|
-
if (
|
|
60919
|
+
if (vote === "undecided") return {
|
|
60920
|
+
...rotated,
|
|
60921
|
+
undecided: row.undecided + 1,
|
|
60922
|
+
updatedAt: observation.timestamp
|
|
60923
|
+
};
|
|
60924
|
+
const seen = row.seen + 1;
|
|
60925
|
+
if (vote === "disagree") return {
|
|
60851
60926
|
...rotated,
|
|
60852
60927
|
seen,
|
|
60853
60928
|
windowSeen: rotated.windowSeen + 1,
|
|
@@ -60895,6 +60970,7 @@ function newEvidenceRow(observation, anchorId) {
|
|
|
60895
60970
|
...observation.label !== void 0 ? { label: observation.label } : {},
|
|
60896
60971
|
sessionAgreed: observation.source === "session" ? 1 : 0,
|
|
60897
60972
|
burstAgreed: observation.source === "burst" ? 1 : 0,
|
|
60973
|
+
undecided: 0,
|
|
60898
60974
|
updatedAt: observation.timestamp
|
|
60899
60975
|
};
|
|
60900
60976
|
}
|
|
@@ -60967,6 +61043,10 @@ var STATIONARY_EVIDENCE_COLUMNS = [
|
|
|
60967
61043
|
name: "burstAgreed",
|
|
60968
61044
|
type: "INTEGER"
|
|
60969
61045
|
},
|
|
61046
|
+
{
|
|
61047
|
+
name: "undecided",
|
|
61048
|
+
type: "INTEGER"
|
|
61049
|
+
},
|
|
60970
61050
|
{
|
|
60971
61051
|
name: "updatedAt",
|
|
60972
61052
|
type: "INTEGER"
|
|
@@ -61014,6 +61094,9 @@ var StationaryEvidenceLedger = class {
|
|
|
61014
61094
|
* every observation of a box that never changes (3836's jacket is seen five
|
|
61015
61095
|
* times a second in a session), so the line is said once per candidate. */
|
|
61016
61096
|
implausibleReported = /* @__PURE__ */ new Set();
|
|
61097
|
+
/** `<deviceId>:<trackId>` of tracks whose first undecided fold was logged
|
|
61098
|
+
* (D547). Pruned with the bindings in {@link retainTracks}. */
|
|
61099
|
+
undecidedReported = /* @__PURE__ */ new Set();
|
|
61017
61100
|
anchorSeq = 0;
|
|
61018
61101
|
constructor(deps) {
|
|
61019
61102
|
this.logger = deps.logger;
|
|
@@ -61042,6 +61125,7 @@ var StationaryEvidenceLedger = class {
|
|
|
61042
61125
|
...row.label !== void 0 ? { label: row.label } : {},
|
|
61043
61126
|
sessionAgreed: row.sessionAgreed,
|
|
61044
61127
|
burstAgreed: row.burstAgreed,
|
|
61128
|
+
undecided: row.undecided,
|
|
61045
61129
|
updatedAt: row.updatedAt
|
|
61046
61130
|
}),
|
|
61047
61131
|
fromRecord: (key, data) => {
|
|
@@ -61075,6 +61159,7 @@ var StationaryEvidenceLedger = class {
|
|
|
61075
61159
|
...label !== void 0 ? { label } : {},
|
|
61076
61160
|
sessionAgreed: numberOr(data.sessionAgreed, 0),
|
|
61077
61161
|
burstAgreed: numberOr(data.burstAgreed, 0),
|
|
61162
|
+
undecided: numberOr(data.undecided, 0),
|
|
61078
61163
|
updatedAt
|
|
61079
61164
|
};
|
|
61080
61165
|
},
|
|
@@ -61209,8 +61294,10 @@ var StationaryEvidenceLedger = class {
|
|
|
61209
61294
|
}
|
|
61210
61295
|
if (bindKey !== void 0) this.binding.set(bindKey, row.key);
|
|
61211
61296
|
}
|
|
61212
|
-
const
|
|
61213
|
-
const
|
|
61297
|
+
const vote = observationVote(overlap, policy.agreeIou, observation.source, observation.movementState, observation.settling === true);
|
|
61298
|
+
const agrees = vote === "agree";
|
|
61299
|
+
if (vote === "undecided") this.noteUndecided(row, observation);
|
|
61300
|
+
const next = foldObservation(row, observation, vote, overlap);
|
|
61214
61301
|
this.ledger.stage(next);
|
|
61215
61302
|
this.dirty.add(next.key);
|
|
61216
61303
|
if (created) this.capDevice(observation.deviceId);
|
|
@@ -61260,6 +61347,10 @@ var StationaryEvidenceLedger = class {
|
|
|
61260
61347
|
if (!bindKey.startsWith(prefix)) continue;
|
|
61261
61348
|
if (!liveTrackIds.has(bindKey.slice(prefix.length))) this.binding.delete(bindKey);
|
|
61262
61349
|
}
|
|
61350
|
+
for (const key of [...this.undecidedReported]) {
|
|
61351
|
+
if (!key.startsWith(prefix)) continue;
|
|
61352
|
+
if (!liveTrackIds.has(key.slice(prefix.length))) this.undecidedReported.delete(key);
|
|
61353
|
+
}
|
|
61263
61354
|
}
|
|
61264
61355
|
forgetKey(key) {
|
|
61265
61356
|
this.forgetKeyDurable(key);
|
|
@@ -61272,6 +61363,32 @@ var StationaryEvidenceLedger = class {
|
|
|
61272
61363
|
* work is never silent (D391), and one that refuses it five times a second
|
|
61273
61364
|
* is never loud.
|
|
61274
61365
|
*/
|
|
61366
|
+
/**
|
|
61367
|
+
* An undecided observation is dropped from the tally on purpose (D547), and
|
|
61368
|
+
* a branch that drops work says so — once per track, at debug: the same
|
|
61369
|
+
* track offers one every frame until it decides, and a line per frame would
|
|
61370
|
+
* bury what it reports. The lifetime count rides the row (`undecided`).
|
|
61371
|
+
*/
|
|
61372
|
+
noteUndecided(row, observation) {
|
|
61373
|
+
const trackId = observation.trackId;
|
|
61374
|
+
if (trackId === void 0) return;
|
|
61375
|
+
const key = `${observation.deviceId}:${trackId}`;
|
|
61376
|
+
if (this.undecidedReported.has(key)) return;
|
|
61377
|
+
this.undecidedReported.add(key);
|
|
61378
|
+
this.logger.debug("stationary evidence: undecided observation — counted as neither N nor M", {
|
|
61379
|
+
tags: { deviceId: observation.deviceId },
|
|
61380
|
+
meta: {
|
|
61381
|
+
deviceId: observation.deviceId,
|
|
61382
|
+
trackId,
|
|
61383
|
+
candidateKey: row.key,
|
|
61384
|
+
movementState: observation.movementState ?? null,
|
|
61385
|
+
settling: observation.settling === true,
|
|
61386
|
+
agreed: row.agreed,
|
|
61387
|
+
seen: row.seen,
|
|
61388
|
+
undecided: row.undecided
|
|
61389
|
+
}
|
|
61390
|
+
});
|
|
61391
|
+
}
|
|
61275
61392
|
noteImplausible(row, frame) {
|
|
61276
61393
|
if (this.implausibleReported.has(row.key)) return;
|
|
61277
61394
|
this.implausibleReported.add(row.key);
|
|
@@ -61344,6 +61461,65 @@ var StationaryEvidenceLedger = class {
|
|
|
61344
61461
|
for (const row of excess) this.forgetKey(row.key);
|
|
61345
61462
|
}
|
|
61346
61463
|
};
|
|
61464
|
+
//#endregion
|
|
61465
|
+
//#region src/pipeline-analytics/pipeline/stationary/stationary-track-disposal.ts
|
|
61466
|
+
function planStationaryTrackDisposal(input) {
|
|
61467
|
+
const trackIds = [input.liveTrackId];
|
|
61468
|
+
if (input.sourceTrackId !== void 0 && input.sourceTrackId !== input.liveTrackId) trackIds.push(input.sourceTrackId);
|
|
61469
|
+
const keepMediaIds = /* @__PURE__ */ new Set();
|
|
61470
|
+
if (input.entryKeyFrameMediaId !== void 0) keepMediaIds.add(input.entryKeyFrameMediaId);
|
|
61471
|
+
return {
|
|
61472
|
+
trackIds,
|
|
61473
|
+
keepMediaIds
|
|
61474
|
+
};
|
|
61475
|
+
}
|
|
61476
|
+
/**
|
|
61477
|
+
* Run the plan. The live track is dropped from RAM FIRST so it can never be
|
|
61478
|
+
* persisted by a sweep racing this call; the cascade then removes what every
|
|
61479
|
+
* track owned, sparing the entry's key frame, and deletes the row (a no-op for
|
|
61480
|
+
* the never-persisted live track).
|
|
61481
|
+
*
|
|
61482
|
+
* Never silent: the line names the entity, what went and what was kept — a
|
|
61483
|
+
* deleted track is dropped work, and the operator asked for it by name.
|
|
61484
|
+
*/
|
|
61485
|
+
async function disposeStationaryTracks(deps, plan) {
|
|
61486
|
+
const [liveTrackId] = plan.trackIds;
|
|
61487
|
+
if (liveTrackId !== void 0) deps.trackStore.dropActive(liveTrackId);
|
|
61488
|
+
const sparingMedia = { deleteByTracks: (trackIds) => deps.media.deleteByTracks(trackIds, { spare: plan.keepMediaIds }) };
|
|
61489
|
+
const result = await runTrackCascadeBatch({
|
|
61490
|
+
registry: [...deps.leaves, sparingMedia],
|
|
61491
|
+
trackStore: deps.trackStore,
|
|
61492
|
+
deviceId: deps.deviceId,
|
|
61493
|
+
onTrackCleanup: deps.onTrackCleanup,
|
|
61494
|
+
onFailure: (trackId, err) => {
|
|
61495
|
+
deps.logger.warn("stationary promotion: a source track could not be disposed of", {
|
|
61496
|
+
tags: { deviceId: deps.deviceId },
|
|
61497
|
+
meta: {
|
|
61498
|
+
deviceId: deps.deviceId,
|
|
61499
|
+
entryId: deps.entryId,
|
|
61500
|
+
trackId,
|
|
61501
|
+
error: String(err)
|
|
61502
|
+
}
|
|
61503
|
+
});
|
|
61504
|
+
}
|
|
61505
|
+
}, plan.trackIds);
|
|
61506
|
+
deps.logger.info("stationary promotion: source tracks discarded — the entity keeps its key frame", {
|
|
61507
|
+
tags: { deviceId: deps.deviceId },
|
|
61508
|
+
meta: {
|
|
61509
|
+
deviceId: deps.deviceId,
|
|
61510
|
+
entryId: deps.entryId,
|
|
61511
|
+
trackIds: plan.trackIds,
|
|
61512
|
+
keptMediaIds: [...plan.keepMediaIds],
|
|
61513
|
+
deleted: result.deleted,
|
|
61514
|
+
failed: result.failed,
|
|
61515
|
+
mediaRows: result.tally.media
|
|
61516
|
+
}
|
|
61517
|
+
});
|
|
61518
|
+
return {
|
|
61519
|
+
deleted: result.deleted,
|
|
61520
|
+
failed: result.failed
|
|
61521
|
+
};
|
|
61522
|
+
}
|
|
61347
61523
|
/**
|
|
61348
61524
|
* How long the same weak box must keep being seen before it may re-anchor.
|
|
61349
61525
|
*
|
|
@@ -61523,6 +61699,7 @@ var StationaryObjectRegistry = class {
|
|
|
61523
61699
|
sessionGapMs;
|
|
61524
61700
|
warmUpMs;
|
|
61525
61701
|
ttlForDevice;
|
|
61702
|
+
onEntryDropped;
|
|
61526
61703
|
onChange;
|
|
61527
61704
|
requestConfirmationLook;
|
|
61528
61705
|
/**
|
|
@@ -61616,6 +61793,7 @@ var StationaryObjectRegistry = class {
|
|
|
61616
61793
|
this.sessionGapMs = deps.sessionGapMs ?? 5e3;
|
|
61617
61794
|
this.warmUpMs = deps.warmUpMs ?? 2e3;
|
|
61618
61795
|
this.ttlForDevice = deps.ttlForDevice;
|
|
61796
|
+
this.onEntryDropped = deps.onEntryDropped;
|
|
61619
61797
|
this.onChange = deps.onChange;
|
|
61620
61798
|
this.requestConfirmationLook = deps.requestConfirmationLook;
|
|
61621
61799
|
}
|
|
@@ -62225,6 +62403,7 @@ var StationaryObjectRegistry = class {
|
|
|
62225
62403
|
return at !== void 0 && at > entry.lastConfirmedAt;
|
|
62226
62404
|
}
|
|
62227
62405
|
dropEntry(deviceId, id) {
|
|
62406
|
+
const dropped = this.ledger.get(id);
|
|
62228
62407
|
this.idsByDevice.get(deviceId)?.delete(id);
|
|
62229
62408
|
this.dirty.delete(id);
|
|
62230
62409
|
this.weakRuns.delete(id);
|
|
@@ -62234,6 +62413,7 @@ var StationaryObjectRegistry = class {
|
|
|
62234
62413
|
if (armed.size === 0) this.departureStrikes.delete(deviceId);
|
|
62235
62414
|
}
|
|
62236
62415
|
this.ledger.forget(id);
|
|
62416
|
+
if (dropped !== void 0) this.onEntryDropped?.(dropped);
|
|
62237
62417
|
}
|
|
62238
62418
|
};
|
|
62239
62419
|
/** The persisted column map for an entry (the `id` PK is passed separately). */
|
|
@@ -68166,8 +68346,10 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
68166
68346
|
let liveTracks;
|
|
68167
68347
|
const liveEvents = /* @__PURE__ */ new Map();
|
|
68168
68348
|
let liveSummaries;
|
|
68349
|
+
let keptMedia = /* @__PURE__ */ new Set();
|
|
68169
68350
|
try {
|
|
68170
68351
|
liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
|
|
68352
|
+
if (deps.keptMediaKeys !== void 0 && scope.collection === "pipeline-analytics:media") keptMedia = await deps.keptMediaKeys(rows.map((row) => row.key));
|
|
68171
68353
|
for (const [table, ids] of eventIdsByTable) {
|
|
68172
68354
|
if (ids.length === 0) continue;
|
|
68173
68355
|
liveEvents.set(table, await deps.liveEventIds(table, ids));
|
|
@@ -68200,7 +68382,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
68200
68382
|
tally.unresolvable += 1;
|
|
68201
68383
|
continue;
|
|
68202
68384
|
}
|
|
68203
|
-
if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.get(ref.table)?.has(ref.eventId) ?? false : ref.kind === "summary" ? liveSummaries.has(ref.summaryId) : false) continue;
|
|
68385
|
+
if (ref.kind === "track" ? liveTracks.has(ref.trackId) || keptMedia.has(row.key) : ref.kind === "event" ? liveEvents.get(ref.table)?.has(ref.eventId) ?? false : ref.kind === "summary" ? liveSummaries.has(ref.summaryId) : false) continue;
|
|
68204
68386
|
const ts = num(row.data, scope.timestampField);
|
|
68205
68387
|
if (ts !== null && ts > graceFloor) {
|
|
68206
68388
|
tally.withinGrace += 1;
|
|
@@ -72526,7 +72708,7 @@ function buildDetectionSettingsSections() {
|
|
|
72526
72708
|
type: "number",
|
|
72527
72709
|
key: "stationarySpeedPx",
|
|
72528
72710
|
label: "Stationary speed",
|
|
72529
|
-
description: "Speed
|
|
72711
|
+
description: "Speed below which a track is treated as stationary and its prediction is frozen, so bbox jitter cannot walk the box off a sitting object. The unit is pixels per REPORT, and this camera decides how often it reports — the same number means 35.7 px/s on a fast camera and 0.94 px/s on an armed-on-motion one. Set it to 0 to judge this camera on its calibrated stillness bar instead (Stationary → Calibrate), which is measured per second and relative to the subject’s own size. With no calibration, 0 keeps the shipped default.",
|
|
72530
72712
|
min: 0,
|
|
72531
72713
|
step: .5,
|
|
72532
72714
|
default: TRACKING_DEFAULTS.stationarySpeedPx,
|
|
@@ -77934,10 +78116,149 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77934
78116
|
const last = track.positions.at(-1)?.bbox;
|
|
77935
78117
|
if (!last) return false;
|
|
77936
78118
|
return bboxMatchesParkedEntry(last, track.className, this.stationaryRegistry?.listViews(track.deviceId) ?? []);
|
|
77937
|
-
}
|
|
78119
|
+
},
|
|
78120
|
+
discardTracks: (tracks) => this.cascadeDiscardedTracks(tracks)
|
|
77938
78121
|
});
|
|
77939
78122
|
return this._trackCloser;
|
|
77940
78123
|
}
|
|
78124
|
+
/**
|
|
78125
|
+
* Cascade what `expireStale` refused to persist (D547): scenery re-fires and
|
|
78126
|
+
* re-sightings of a censused object. Per device, per track, through the
|
|
78127
|
+
* shipped registry — never a raw delete — so the vector, the crops and the
|
|
78128
|
+
* events go with the media. A track whose cascade fails is named.
|
|
78129
|
+
*/
|
|
78130
|
+
async cascadeDiscardedTracks(tracks) {
|
|
78131
|
+
const cascade = this.buildTrackCascadeRegistry();
|
|
78132
|
+
if (!cascade) {
|
|
78133
|
+
this.ctx.logger.warn("discarded tracks could not be cascaded — stores not ready", { meta: { trackIds: tracks.map((t) => t.trackId) } });
|
|
78134
|
+
return;
|
|
78135
|
+
}
|
|
78136
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
78137
|
+
for (const t of tracks) {
|
|
78138
|
+
const ids = byDevice.get(t.deviceId) ?? [];
|
|
78139
|
+
ids.push(t.trackId);
|
|
78140
|
+
byDevice.set(t.deviceId, ids);
|
|
78141
|
+
}
|
|
78142
|
+
for (const [deviceId, trackIds] of byDevice) {
|
|
78143
|
+
const result = await runTrackCascadeBatch({
|
|
78144
|
+
registry: cascade.registry,
|
|
78145
|
+
trackStore: cascade.trackStore,
|
|
78146
|
+
deviceId,
|
|
78147
|
+
onTrackCleanup: (id, trackId) => this.clearLiveTrackState(id, trackId),
|
|
78148
|
+
onFailure: (trackId, err) => {
|
|
78149
|
+
this.ctx.logger.warn("discarded track could not be cascaded", {
|
|
78150
|
+
tags: { deviceId },
|
|
78151
|
+
meta: {
|
|
78152
|
+
deviceId,
|
|
78153
|
+
trackId,
|
|
78154
|
+
error: String(err)
|
|
78155
|
+
}
|
|
78156
|
+
});
|
|
78157
|
+
}
|
|
78158
|
+
}, trackIds);
|
|
78159
|
+
this.ctx.logger.info("tracks discarded on expiry — still objects are entities, not tracks", {
|
|
78160
|
+
tags: { deviceId },
|
|
78161
|
+
meta: {
|
|
78162
|
+
deviceId,
|
|
78163
|
+
trackIds,
|
|
78164
|
+
deleted: result.deleted,
|
|
78165
|
+
failed: result.failed,
|
|
78166
|
+
mediaRows: result.tally.media
|
|
78167
|
+
}
|
|
78168
|
+
});
|
|
78169
|
+
}
|
|
78170
|
+
}
|
|
78171
|
+
/**
|
|
78172
|
+
* Dispose of the tracks that carried a promoted candidate's evidence (D547):
|
|
78173
|
+
* the one alive on the tipping frame and the persisted source of the entry's
|
|
78174
|
+
* best box. The entry's key frame is spared — it is the one row the entity
|
|
78175
|
+
* vouches for, and `keptStationaryMediaKeys` keeps the orphan audit off it.
|
|
78176
|
+
*/
|
|
78177
|
+
async disposePromotedTracks(deviceId, entry, liveTrackId, sourceTrackId) {
|
|
78178
|
+
const cascade = this.buildTrackCascadeRegistry();
|
|
78179
|
+
const mediaStore = this.mediaStore;
|
|
78180
|
+
if (!cascade || !mediaStore) {
|
|
78181
|
+
this.ctx.logger.warn("stationary promotion: source tracks could not be disposed of — stores not ready", {
|
|
78182
|
+
tags: { deviceId },
|
|
78183
|
+
meta: {
|
|
78184
|
+
deviceId,
|
|
78185
|
+
entryId: entry.id,
|
|
78186
|
+
liveTrackId,
|
|
78187
|
+
sourceTrackId: sourceTrackId ?? null
|
|
78188
|
+
}
|
|
78189
|
+
});
|
|
78190
|
+
return;
|
|
78191
|
+
}
|
|
78192
|
+
const plan = planStationaryTrackDisposal({
|
|
78193
|
+
liveTrackId,
|
|
78194
|
+
...sourceTrackId !== void 0 ? { sourceTrackId } : {},
|
|
78195
|
+
...entry.keyFrameMediaId !== void 0 ? { entryKeyFrameMediaId: entry.keyFrameMediaId } : {}
|
|
78196
|
+
});
|
|
78197
|
+
await disposeStationaryTracks({
|
|
78198
|
+
deviceId,
|
|
78199
|
+
entryId: entry.id,
|
|
78200
|
+
leaves: cascade.registry.filter((leaf) => leaf !== mediaStore),
|
|
78201
|
+
media: mediaStore,
|
|
78202
|
+
trackStore: cascade.trackStore,
|
|
78203
|
+
onTrackCleanup: (id, trackId) => this.clearLiveTrackState(id, trackId),
|
|
78204
|
+
logger: this.ctx.logger
|
|
78205
|
+
}, plan);
|
|
78206
|
+
}
|
|
78207
|
+
/**
|
|
78208
|
+
* Media row keys a live stationary entry vouches for (D547) — the orphan
|
|
78209
|
+
* audit's probe. Answered from the registry's RAM index, which is the
|
|
78210
|
+
* authority on which entries are alive.
|
|
78211
|
+
*/
|
|
78212
|
+
keptStationaryMediaKeys(keys) {
|
|
78213
|
+
const registry = this.stationaryRegistry;
|
|
78214
|
+
if (!registry) return /* @__PURE__ */ new Set();
|
|
78215
|
+
const held = /* @__PURE__ */ new Set();
|
|
78216
|
+
for (const deviceId of registry.deviceIds()) for (const entry of registry.list(deviceId)) if (entry.keyFrameMediaId !== void 0) held.add(String(entry.keyFrameMediaId));
|
|
78217
|
+
return new Set(keys.filter((key) => held.has(key)));
|
|
78218
|
+
}
|
|
78219
|
+
/**
|
|
78220
|
+
* An entry left the registry: free the key frame it vouched for (D547),
|
|
78221
|
+
* unless another live entry still holds it or a track row still owns it
|
|
78222
|
+
* (then retention's cascade will). Never silent either way.
|
|
78223
|
+
*/
|
|
78224
|
+
async releaseStationaryKeyFrame(entry) {
|
|
78225
|
+
const mediaId = entry.keyFrameMediaId;
|
|
78226
|
+
if (mediaId === void 0) return;
|
|
78227
|
+
const deviceId = entry.deviceId;
|
|
78228
|
+
const registry = this.stationaryRegistry;
|
|
78229
|
+
const mediaStore = this.mediaStore;
|
|
78230
|
+
const trackStore = this.trackStore;
|
|
78231
|
+
if (!registry || !mediaStore || !trackStore) return;
|
|
78232
|
+
if (registry.list(deviceId).some((e) => e.keyFrameMediaId === mediaId)) return;
|
|
78233
|
+
try {
|
|
78234
|
+
const owner = await mediaStore.ownerOf(mediaId);
|
|
78235
|
+
if (owner === null) return;
|
|
78236
|
+
if (owner.ownerKind === "track") {
|
|
78237
|
+
if ((await trackStore.existingTrackIds([owner.ownerId])).has(owner.ownerId)) return;
|
|
78238
|
+
}
|
|
78239
|
+
await mediaStore.deleteById(mediaId);
|
|
78240
|
+
this.ctx.logger.info("stationary entry released its key frame", {
|
|
78241
|
+
tags: { deviceId },
|
|
78242
|
+
meta: {
|
|
78243
|
+
deviceId,
|
|
78244
|
+
entryId: entry.id,
|
|
78245
|
+
mediaId,
|
|
78246
|
+
ownerKind: owner.ownerKind,
|
|
78247
|
+
ownerId: owner.ownerId
|
|
78248
|
+
}
|
|
78249
|
+
});
|
|
78250
|
+
} catch (err) {
|
|
78251
|
+
this.ctx.logger.warn("stationary entry key frame could not be released", {
|
|
78252
|
+
tags: { deviceId },
|
|
78253
|
+
meta: {
|
|
78254
|
+
deviceId,
|
|
78255
|
+
entryId: entry.id,
|
|
78256
|
+
mediaId,
|
|
78257
|
+
error: String(err)
|
|
78258
|
+
}
|
|
78259
|
+
});
|
|
78260
|
+
}
|
|
78261
|
+
}
|
|
77941
78262
|
/** Master toggle for the migrated object/face EmbeddingDispatcher. Read at
|
|
77942
78263
|
* init; a change takes effect on addon restart (same convention as
|
|
77943
78264
|
* `postProcessingNodeId`).
|
|
@@ -78663,6 +78984,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78663
78984
|
store: api.settingsStore,
|
|
78664
78985
|
logger: logger.child("StationaryRegistry"),
|
|
78665
78986
|
ttlForDevice: (deviceId) => this.stationarySettingsFromCache(deviceId).entryTtlMs,
|
|
78987
|
+
onEntryDropped: (entry) => {
|
|
78988
|
+
this.releaseStationaryKeyFrame(entry);
|
|
78989
|
+
},
|
|
78666
78990
|
requestConfirmationLook: (ask) => this.requestConfirmationLook(ask),
|
|
78667
78991
|
onChange: ({ phase, entry, timestamp, observedAt }) => {
|
|
78668
78992
|
this.ctx.eventBus.emit({
|
|
@@ -80051,6 +80375,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
80051
80375
|
anchor: { ...row.anchor },
|
|
80052
80376
|
agreed: row.agreed,
|
|
80053
80377
|
seen: row.seen,
|
|
80378
|
+
undecided: row.undecided,
|
|
80054
80379
|
ratio: row.seen > 0 ? row.agreed / row.seen : 0,
|
|
80055
80380
|
spanMs: row.lastAgreedAt - row.firstAgreedAt,
|
|
80056
80381
|
firstAgreedAt: row.firstAgreedAt,
|
|
@@ -81451,7 +81776,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
81451
81776
|
frameHeight: result.frameHeight,
|
|
81452
81777
|
...observedKeyFrame !== void 0 ? { keyFrameMediaId: observedKeyFrame } : {},
|
|
81453
81778
|
...observedLabel !== void 0 ? { label: observedLabel } : {},
|
|
81454
|
-
...movementState !== void 0 ? { movementState } : {}
|
|
81779
|
+
...movementState !== void 0 ? { movementState } : {},
|
|
81780
|
+
settling: t.settling
|
|
81455
81781
|
};
|
|
81456
81782
|
const outcome = evidence.observe(observation);
|
|
81457
81783
|
if (outcome === void 0 || !outcome.promoted) continue;
|
|
@@ -84601,17 +84927,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
84601
84927
|
wakeAssocIou: gate.wakeAssocIou
|
|
84602
84928
|
});
|
|
84603
84929
|
processor.dropTrack(track.trackId);
|
|
84604
|
-
this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
|
|
84605
|
-
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
84606
|
-
tags: { deviceId },
|
|
84607
|
-
meta: {
|
|
84608
|
-
deviceId,
|
|
84609
|
-
trackId: track.trackId,
|
|
84610
|
-
entryId: entry.id,
|
|
84611
|
-
error: String(err)
|
|
84612
|
-
}
|
|
84613
|
-
});
|
|
84614
|
-
});
|
|
84615
84930
|
const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, track.trackId);
|
|
84616
84931
|
const dropKeyFrame = () => {
|
|
84617
84932
|
this.residents.dropKeyFrameMediaId(track.trackId);
|
|
@@ -84619,7 +84934,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
84619
84934
|
if (faceEnd) faceEnd.finally(dropKeyFrame);
|
|
84620
84935
|
else dropKeyFrame();
|
|
84621
84936
|
const promotedPath = this.trackStore?.getActiveByTrack(track.trackId)?.positions;
|
|
84622
|
-
this.plateRecognizer?.onTrackEnd(deviceId, track.trackId, promotedPath);
|
|
84937
|
+
const plateEnd = this.plateRecognizer?.onTrackEnd(deviceId, track.trackId, promotedPath);
|
|
84938
|
+
Promise.allSettled([faceEnd, plateEnd]).then(() => this.disposePromotedTracks(deviceId, entry, track.trackId, row.sourceTrackId));
|
|
84623
84939
|
this.residents.close(track.trackId, "stationary");
|
|
84624
84940
|
this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
|
|
84625
84941
|
this.overlayState.onTrackEnded(deviceId, track.trackId);
|
|
@@ -84662,6 +84978,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
84662
84978
|
rescueCentroidFactor: trk.rescueCentroidFactor,
|
|
84663
84979
|
resurrectionWindowMs: trk.resurrectionWindowMs,
|
|
84664
84980
|
stationarySpeedPx: trk.stationarySpeedPx,
|
|
84981
|
+
...stillSpeedFracPerSec !== void 0 ? { stillSpeedFracPerSec } : {},
|
|
84665
84982
|
maxTrackLifetimeMs: trk.maxTrackLifetimeSec * 1e3,
|
|
84666
84983
|
dedupEnabled: trk.dedupEnabled,
|
|
84667
84984
|
dedupSpawnIou: trk.dedupSpawnIou,
|
|
@@ -85561,6 +85878,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
85561
85878
|
if (!summaries) throw new Error("summary store not built — probe unavailable");
|
|
85562
85879
|
return summaries.existingIds(ids);
|
|
85563
85880
|
},
|
|
85881
|
+
keptMediaKeys: async (keys) => this.keptStationaryMediaKeys(keys),
|
|
85564
85882
|
reclaim: buildOrphanReclaimer({
|
|
85565
85883
|
deleteFace: async (faceId) => {
|
|
85566
85884
|
await this.faceStore?.delete(faceId);
|