@camstack/addon-post-analysis 1.2.270 → 1.2.272
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-DsMoBsZn.mjs → dist-4BOLDrB1.mjs} +366 -15
- package/dist/{dist-C-nKNO5A.js → dist-BkBZcyAK.js} +366 -15
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-kmC6aaLp.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D2l0wRbH.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BePOftX5.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DJJiuYPX.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-Do_HGiCP.mjs → hostInit-DW0buByp.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +401 -77
- package/dist/pipeline-analytics/index.mjs +401 -77
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-VNIUqm1k.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C5WB_ysd.mjs +0 -26
|
@@ -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-4BOLDrB1.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,
|
|
@@ -39361,7 +39384,9 @@ var FrameProcessor = class {
|
|
|
39361
39384
|
const refusals = [];
|
|
39362
39385
|
const labelPatchByTrack = /* @__PURE__ */ new Map();
|
|
39363
39386
|
const tracked = trackedDetections.map((td) => {
|
|
39364
|
-
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";
|
|
39365
39390
|
const candidate = resolveDetectionLabelCandidate({
|
|
39366
39391
|
className: td.class,
|
|
39367
39392
|
originalClass: td.originalClass,
|
|
@@ -39399,6 +39424,7 @@ var FrameProcessor = class {
|
|
|
39399
39424
|
zones: zonesByTrack.get(td.trackId) ?? [],
|
|
39400
39425
|
state,
|
|
39401
39426
|
matchedThisFrame: td.matchedThisFrame !== false,
|
|
39427
|
+
settling,
|
|
39402
39428
|
...td.resurrectionScene !== void 0 ? { resurrectionScene: td.resurrectionScene } : {},
|
|
39403
39429
|
...labelPatch !== void 0 ? { labelPatch } : {},
|
|
39404
39430
|
...emb !== void 0 ? {
|
|
@@ -40394,8 +40420,16 @@ var TrackCloser = class {
|
|
|
40394
40420
|
try {
|
|
40395
40421
|
const trackStore = this.deps.trackStore();
|
|
40396
40422
|
if (!trackStore) return;
|
|
40397
|
-
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
|
+
});
|
|
40398
40431
|
for (const t of expired) await this.closeExpired(t);
|
|
40432
|
+
if (discardTracks !== void 0 && discarded.length > 0) await discardTracks(discarded);
|
|
40399
40433
|
} catch (err) {
|
|
40400
40434
|
if (this.deps.isShuttingDown()) return;
|
|
40401
40435
|
this.deps.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
@@ -44093,6 +44127,19 @@ var VEHICLE_CLASSES$1 = new Set([
|
|
|
44093
44127
|
"truck",
|
|
44094
44128
|
"bus"
|
|
44095
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
|
+
}
|
|
44096
44143
|
function motionlessMaxPxForClass(className) {
|
|
44097
44144
|
return VEHICLE_CLASSES$1.has(className.toLowerCase()) ? 8 : 40;
|
|
44098
44145
|
}
|
|
@@ -44850,7 +44897,30 @@ var TrackStore = class {
|
|
|
44850
44897
|
for (const [trackId, t] of this.active) {
|
|
44851
44898
|
if (nowMs - t.lastSeen < this.config.ttlMs) continue;
|
|
44852
44899
|
t.active = false;
|
|
44853
|
-
|
|
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";
|
|
44854
44924
|
const record = cloneTrack(t);
|
|
44855
44925
|
try {
|
|
44856
44926
|
await this.persistCompleted(record);
|
|
@@ -47725,6 +47795,8 @@ function visitsToClips(visits, events, pad, vouched) {
|
|
|
47725
47795
|
* injected fetch/resolve deps so the compose logic is testable without I/O.
|
|
47726
47796
|
*/
|
|
47727
47797
|
var SOURCE = "analytics";
|
|
47798
|
+
/** The addon that registers this provider — stamped on its source row. */
|
|
47799
|
+
var ANALYTICS_ADDON_ID = "pipeline-analytics";
|
|
47728
47800
|
/**
|
|
47729
47801
|
* Backstop on the ANNOTATION read — never on the clip list.
|
|
47730
47802
|
*
|
|
@@ -47886,14 +47958,34 @@ function createVideoclipsProvider(deps) {
|
|
|
47886
47958
|
reportUnvouchedClips(deps, deviceId, answer);
|
|
47887
47959
|
return answer;
|
|
47888
47960
|
},
|
|
47961
|
+
/**
|
|
47962
|
+
* The one source this provider serves, on every camera: the hub's own
|
|
47963
|
+
* analytics visits over recorded footage.
|
|
47964
|
+
*
|
|
47965
|
+
* Always `ok`, and that is not optimism — this source IS the process
|
|
47966
|
+
* answering the call, so there is no reachability question to report. A
|
|
47967
|
+
* read that fails INSIDE `listClips` degrades that list (and logs it); a
|
|
47968
|
+
* source that cannot be reached at all is reported by the collection
|
|
47969
|
+
* dispatcher, which is the only layer that can tell the difference.
|
|
47970
|
+
* `label` is what the picker draws.
|
|
47971
|
+
*/
|
|
47972
|
+
listSources: async () => [{
|
|
47973
|
+
source: SOURCE,
|
|
47974
|
+
addonId: ANALYTICS_ADDON_ID,
|
|
47975
|
+
label: "CamStack recordings",
|
|
47976
|
+
availability: { state: "ok" }
|
|
47977
|
+
}],
|
|
47889
47978
|
getClipPlayback: async ({ deviceId, clipId }) => {
|
|
47890
47979
|
const parsed = parseClipId(clipId);
|
|
47891
47980
|
if (!parsed) throw new Error(`videoclips: unrecognized clipId "${clipId}"`);
|
|
47892
|
-
return
|
|
47893
|
-
|
|
47894
|
-
|
|
47895
|
-
|
|
47896
|
-
|
|
47981
|
+
return {
|
|
47982
|
+
...await deps.resolvePlayback({
|
|
47983
|
+
deviceId,
|
|
47984
|
+
startMs: parsed.startMs,
|
|
47985
|
+
endMs: parsed.endMs
|
|
47986
|
+
}),
|
|
47987
|
+
format: "hls"
|
|
47988
|
+
};
|
|
47897
47989
|
}
|
|
47898
47990
|
};
|
|
47899
47991
|
}
|
|
@@ -53411,6 +53503,7 @@ var FaceRecognizer = class {
|
|
|
53411
53503
|
zones: [],
|
|
53412
53504
|
state: "moving",
|
|
53413
53505
|
matchedThisFrame: true,
|
|
53506
|
+
settling: false,
|
|
53414
53507
|
embedding: input.embedding,
|
|
53415
53508
|
embeddingModelId: modelId,
|
|
53416
53509
|
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
@@ -60805,64 +60898,53 @@ function stillnessVerdictFor(source, trackState) {
|
|
|
60805
60898
|
return source === "burst" ? void 0 : trackState;
|
|
60806
60899
|
}
|
|
60807
60900
|
/**
|
|
60808
|
-
*
|
|
60809
|
-
*
|
|
60810
|
-
* Two questions, deliberately kept apart:
|
|
60811
|
-
*
|
|
60812
|
-
* - IDENTITY — is this the same object in the same place? That is the IoU,
|
|
60813
|
-
* and it is the only notion of sameness in this module.
|
|
60814
|
-
* - STILLNESS — is it going anywhere? On a session frame that is the
|
|
60815
|
-
* TRACKER's verdict, and nothing here re-derives it. On a burst frame it is
|
|
60816
|
-
* the OCCASION: the runner rechecks a still scene and nothing else (see
|
|
60817
|
-
* {@link stillnessVerdictFor}).
|
|
60901
|
+
* What one observation is worth to a candidate: `agree` (N and M), `disagree`
|
|
60902
|
+
* (M only) or `undecided` (neither).
|
|
60818
60903
|
*
|
|
60819
|
-
* ##
|
|
60904
|
+
* ## Why a third answer exists (D547)
|
|
60820
60905
|
*
|
|
60821
|
-
*
|
|
60822
|
-
*
|
|
60823
|
-
* the
|
|
60824
|
-
*
|
|
60825
|
-
*
|
|
60826
|
-
*
|
|
60827
|
-
*
|
|
60828
|
-
*
|
|
60829
|
-
*
|
|
60830
|
-
*
|
|
60831
|
-
* no number of its own.
|
|
60906
|
+
* The ratio N/M exists to dilute a car that merely drove through the spot.
|
|
60907
|
+
* It was also diluting a car that had not yet had the CHANCE to be called
|
|
60908
|
+
* still: the five `entered` frames every track begins with, and every frame
|
|
60909
|
+
* on which the analyzer's settled test already passed but
|
|
60910
|
+
* `stationaryThresholdSec` had not elapsed — `moving` in name only. On a
|
|
60911
|
+
* session-dispatched camera those frames are a third of every 36 s track, and
|
|
60912
|
+
* a session cut short by a passer-by is made of nothing else. Measured on
|
|
60913
|
+
* device 617 (DNRG309, 2026-09-19 20:32 → 09-20 07:53): 13 of the 21 tracks
|
|
60914
|
+
* bound to the candidate never reached `idle`, the ledger's ratio was 0.493
|
|
60915
|
+
* against 0.6, and a parked car took 11.3 h and 19 tracks to be censused.
|
|
60832
60916
|
*
|
|
60833
|
-
*
|
|
60834
|
-
*
|
|
60835
|
-
*
|
|
60836
|
-
*
|
|
60837
|
-
* passing the edge is gone in ~8 frames, five of them `entered`, and 5/8 = 0.62
|
|
60838
|
-
* cleared the 0.6 ratio unaided — an entry born on an empty spot, retired 24
|
|
60839
|
-
* absent readings later, one "parked" and one "departed" notification for a
|
|
60840
|
-
* passage. A parked car's re-detection loses five observations of ~280.
|
|
60841
|
-
*
|
|
60842
|
-
* A session observation with NO verdict at all does not agree either. There
|
|
60843
|
-
* is no trackless producer any more (the hydrated baseline sampler is gone),
|
|
60844
|
-
* so an absent verdict on a session frame is a producer that forgot the
|
|
60845
|
-
* field — the one way D494 said this could rot — and it must read as a miss,
|
|
60846
|
-
* never as the old geometry-only rule.
|
|
60917
|
+
* "Not yet decided" is not evidence either way, so it counts as neither. A
|
|
60918
|
+
* subject that is TRAVELLING — `moving` with the settled test failing — still
|
|
60919
|
+
* dilutes, so the drive-through case D508 measured (5 `entered` of 8 frames)
|
|
60920
|
+
* now scores 0/3 instead of the 5/8 that once minted an entry on an empty spot.
|
|
60847
60921
|
*/
|
|
60848
|
-
function
|
|
60849
|
-
if (overlap < agreeIou) return
|
|
60850
|
-
if (source === "burst") return
|
|
60851
|
-
|
|
60922
|
+
function observationVote(overlap, agreeIou, source, movementState, settling) {
|
|
60923
|
+
if (overlap < agreeIou) return "disagree";
|
|
60924
|
+
if (source === "burst") return "agree";
|
|
60925
|
+
if (movementState === "idle") return "agree";
|
|
60926
|
+
if (movementState === "entered") return "undecided";
|
|
60927
|
+
if (movementState === "moving" && settling) return "undecided";
|
|
60928
|
+
return "disagree";
|
|
60852
60929
|
}
|
|
60853
60930
|
/**
|
|
60854
60931
|
* Fold one observation into a candidate.
|
|
60855
60932
|
*
|
|
60856
|
-
* `
|
|
60933
|
+
* The `vote` is decided by the CALLER, because M is what makes the ratio mean
|
|
60857
60934
|
* anything: once a track has bound to a candidate, every later observation of
|
|
60858
60935
|
* that track counts toward M whether it agrees or not. That is what dilutes a
|
|
60859
60936
|
* car that merely drove through the spot, and it is why this function cannot
|
|
60860
60937
|
* re-derive agreement from the box alone.
|
|
60861
60938
|
*/
|
|
60862
|
-
function foldObservation(row, observation,
|
|
60863
|
-
const seen = row.seen + 1;
|
|
60939
|
+
function foldObservation(row, observation, vote, overlap) {
|
|
60864
60940
|
const rotated = rotateWindow(row, observation.timestamp);
|
|
60865
|
-
if (
|
|
60941
|
+
if (vote === "undecided") return {
|
|
60942
|
+
...rotated,
|
|
60943
|
+
undecided: row.undecided + 1,
|
|
60944
|
+
updatedAt: observation.timestamp
|
|
60945
|
+
};
|
|
60946
|
+
const seen = row.seen + 1;
|
|
60947
|
+
if (vote === "disagree") return {
|
|
60866
60948
|
...rotated,
|
|
60867
60949
|
seen,
|
|
60868
60950
|
windowSeen: rotated.windowSeen + 1,
|
|
@@ -60910,6 +60992,7 @@ function newEvidenceRow(observation, anchorId) {
|
|
|
60910
60992
|
...observation.label !== void 0 ? { label: observation.label } : {},
|
|
60911
60993
|
sessionAgreed: observation.source === "session" ? 1 : 0,
|
|
60912
60994
|
burstAgreed: observation.source === "burst" ? 1 : 0,
|
|
60995
|
+
undecided: 0,
|
|
60913
60996
|
updatedAt: observation.timestamp
|
|
60914
60997
|
};
|
|
60915
60998
|
}
|
|
@@ -60982,6 +61065,10 @@ var STATIONARY_EVIDENCE_COLUMNS = [
|
|
|
60982
61065
|
name: "burstAgreed",
|
|
60983
61066
|
type: "INTEGER"
|
|
60984
61067
|
},
|
|
61068
|
+
{
|
|
61069
|
+
name: "undecided",
|
|
61070
|
+
type: "INTEGER"
|
|
61071
|
+
},
|
|
60985
61072
|
{
|
|
60986
61073
|
name: "updatedAt",
|
|
60987
61074
|
type: "INTEGER"
|
|
@@ -61029,6 +61116,9 @@ var StationaryEvidenceLedger = class {
|
|
|
61029
61116
|
* every observation of a box that never changes (3836's jacket is seen five
|
|
61030
61117
|
* times a second in a session), so the line is said once per candidate. */
|
|
61031
61118
|
implausibleReported = /* @__PURE__ */ new Set();
|
|
61119
|
+
/** `<deviceId>:<trackId>` of tracks whose first undecided fold was logged
|
|
61120
|
+
* (D547). Pruned with the bindings in {@link retainTracks}. */
|
|
61121
|
+
undecidedReported = /* @__PURE__ */ new Set();
|
|
61032
61122
|
anchorSeq = 0;
|
|
61033
61123
|
constructor(deps) {
|
|
61034
61124
|
this.logger = deps.logger;
|
|
@@ -61057,6 +61147,7 @@ var StationaryEvidenceLedger = class {
|
|
|
61057
61147
|
...row.label !== void 0 ? { label: row.label } : {},
|
|
61058
61148
|
sessionAgreed: row.sessionAgreed,
|
|
61059
61149
|
burstAgreed: row.burstAgreed,
|
|
61150
|
+
undecided: row.undecided,
|
|
61060
61151
|
updatedAt: row.updatedAt
|
|
61061
61152
|
}),
|
|
61062
61153
|
fromRecord: (key, data) => {
|
|
@@ -61090,6 +61181,7 @@ var StationaryEvidenceLedger = class {
|
|
|
61090
61181
|
...label !== void 0 ? { label } : {},
|
|
61091
61182
|
sessionAgreed: numberOr(data.sessionAgreed, 0),
|
|
61092
61183
|
burstAgreed: numberOr(data.burstAgreed, 0),
|
|
61184
|
+
undecided: numberOr(data.undecided, 0),
|
|
61093
61185
|
updatedAt
|
|
61094
61186
|
};
|
|
61095
61187
|
},
|
|
@@ -61224,8 +61316,10 @@ var StationaryEvidenceLedger = class {
|
|
|
61224
61316
|
}
|
|
61225
61317
|
if (bindKey !== void 0) this.binding.set(bindKey, row.key);
|
|
61226
61318
|
}
|
|
61227
|
-
const
|
|
61228
|
-
const
|
|
61319
|
+
const vote = observationVote(overlap, policy.agreeIou, observation.source, observation.movementState, observation.settling === true);
|
|
61320
|
+
const agrees = vote === "agree";
|
|
61321
|
+
if (vote === "undecided") this.noteUndecided(row, observation);
|
|
61322
|
+
const next = foldObservation(row, observation, vote, overlap);
|
|
61229
61323
|
this.ledger.stage(next);
|
|
61230
61324
|
this.dirty.add(next.key);
|
|
61231
61325
|
if (created) this.capDevice(observation.deviceId);
|
|
@@ -61275,6 +61369,10 @@ var StationaryEvidenceLedger = class {
|
|
|
61275
61369
|
if (!bindKey.startsWith(prefix)) continue;
|
|
61276
61370
|
if (!liveTrackIds.has(bindKey.slice(prefix.length))) this.binding.delete(bindKey);
|
|
61277
61371
|
}
|
|
61372
|
+
for (const key of [...this.undecidedReported]) {
|
|
61373
|
+
if (!key.startsWith(prefix)) continue;
|
|
61374
|
+
if (!liveTrackIds.has(key.slice(prefix.length))) this.undecidedReported.delete(key);
|
|
61375
|
+
}
|
|
61278
61376
|
}
|
|
61279
61377
|
forgetKey(key) {
|
|
61280
61378
|
this.forgetKeyDurable(key);
|
|
@@ -61287,6 +61385,32 @@ var StationaryEvidenceLedger = class {
|
|
|
61287
61385
|
* work is never silent (D391), and one that refuses it five times a second
|
|
61288
61386
|
* is never loud.
|
|
61289
61387
|
*/
|
|
61388
|
+
/**
|
|
61389
|
+
* An undecided observation is dropped from the tally on purpose (D547), and
|
|
61390
|
+
* a branch that drops work says so — once per track, at debug: the same
|
|
61391
|
+
* track offers one every frame until it decides, and a line per frame would
|
|
61392
|
+
* bury what it reports. The lifetime count rides the row (`undecided`).
|
|
61393
|
+
*/
|
|
61394
|
+
noteUndecided(row, observation) {
|
|
61395
|
+
const trackId = observation.trackId;
|
|
61396
|
+
if (trackId === void 0) return;
|
|
61397
|
+
const key = `${observation.deviceId}:${trackId}`;
|
|
61398
|
+
if (this.undecidedReported.has(key)) return;
|
|
61399
|
+
this.undecidedReported.add(key);
|
|
61400
|
+
this.logger.debug("stationary evidence: undecided observation — counted as neither N nor M", {
|
|
61401
|
+
tags: { deviceId: observation.deviceId },
|
|
61402
|
+
meta: {
|
|
61403
|
+
deviceId: observation.deviceId,
|
|
61404
|
+
trackId,
|
|
61405
|
+
candidateKey: row.key,
|
|
61406
|
+
movementState: observation.movementState ?? null,
|
|
61407
|
+
settling: observation.settling === true,
|
|
61408
|
+
agreed: row.agreed,
|
|
61409
|
+
seen: row.seen,
|
|
61410
|
+
undecided: row.undecided
|
|
61411
|
+
}
|
|
61412
|
+
});
|
|
61413
|
+
}
|
|
61290
61414
|
noteImplausible(row, frame) {
|
|
61291
61415
|
if (this.implausibleReported.has(row.key)) return;
|
|
61292
61416
|
this.implausibleReported.add(row.key);
|
|
@@ -61359,6 +61483,65 @@ var StationaryEvidenceLedger = class {
|
|
|
61359
61483
|
for (const row of excess) this.forgetKey(row.key);
|
|
61360
61484
|
}
|
|
61361
61485
|
};
|
|
61486
|
+
//#endregion
|
|
61487
|
+
//#region src/pipeline-analytics/pipeline/stationary/stationary-track-disposal.ts
|
|
61488
|
+
function planStationaryTrackDisposal(input) {
|
|
61489
|
+
const trackIds = [input.liveTrackId];
|
|
61490
|
+
if (input.sourceTrackId !== void 0 && input.sourceTrackId !== input.liveTrackId) trackIds.push(input.sourceTrackId);
|
|
61491
|
+
const keepMediaIds = /* @__PURE__ */ new Set();
|
|
61492
|
+
if (input.entryKeyFrameMediaId !== void 0) keepMediaIds.add(input.entryKeyFrameMediaId);
|
|
61493
|
+
return {
|
|
61494
|
+
trackIds,
|
|
61495
|
+
keepMediaIds
|
|
61496
|
+
};
|
|
61497
|
+
}
|
|
61498
|
+
/**
|
|
61499
|
+
* Run the plan. The live track is dropped from RAM FIRST so it can never be
|
|
61500
|
+
* persisted by a sweep racing this call; the cascade then removes what every
|
|
61501
|
+
* track owned, sparing the entry's key frame, and deletes the row (a no-op for
|
|
61502
|
+
* the never-persisted live track).
|
|
61503
|
+
*
|
|
61504
|
+
* Never silent: the line names the entity, what went and what was kept — a
|
|
61505
|
+
* deleted track is dropped work, and the operator asked for it by name.
|
|
61506
|
+
*/
|
|
61507
|
+
async function disposeStationaryTracks(deps, plan) {
|
|
61508
|
+
const [liveTrackId] = plan.trackIds;
|
|
61509
|
+
if (liveTrackId !== void 0) deps.trackStore.dropActive(liveTrackId);
|
|
61510
|
+
const sparingMedia = { deleteByTracks: (trackIds) => deps.media.deleteByTracks(trackIds, { spare: plan.keepMediaIds }) };
|
|
61511
|
+
const result = await runTrackCascadeBatch({
|
|
61512
|
+
registry: [...deps.leaves, sparingMedia],
|
|
61513
|
+
trackStore: deps.trackStore,
|
|
61514
|
+
deviceId: deps.deviceId,
|
|
61515
|
+
onTrackCleanup: deps.onTrackCleanup,
|
|
61516
|
+
onFailure: (trackId, err) => {
|
|
61517
|
+
deps.logger.warn("stationary promotion: a source track could not be disposed of", {
|
|
61518
|
+
tags: { deviceId: deps.deviceId },
|
|
61519
|
+
meta: {
|
|
61520
|
+
deviceId: deps.deviceId,
|
|
61521
|
+
entryId: deps.entryId,
|
|
61522
|
+
trackId,
|
|
61523
|
+
error: String(err)
|
|
61524
|
+
}
|
|
61525
|
+
});
|
|
61526
|
+
}
|
|
61527
|
+
}, plan.trackIds);
|
|
61528
|
+
deps.logger.info("stationary promotion: source tracks discarded — the entity keeps its key frame", {
|
|
61529
|
+
tags: { deviceId: deps.deviceId },
|
|
61530
|
+
meta: {
|
|
61531
|
+
deviceId: deps.deviceId,
|
|
61532
|
+
entryId: deps.entryId,
|
|
61533
|
+
trackIds: plan.trackIds,
|
|
61534
|
+
keptMediaIds: [...plan.keepMediaIds],
|
|
61535
|
+
deleted: result.deleted,
|
|
61536
|
+
failed: result.failed,
|
|
61537
|
+
mediaRows: result.tally.media
|
|
61538
|
+
}
|
|
61539
|
+
});
|
|
61540
|
+
return {
|
|
61541
|
+
deleted: result.deleted,
|
|
61542
|
+
failed: result.failed
|
|
61543
|
+
};
|
|
61544
|
+
}
|
|
61362
61545
|
/**
|
|
61363
61546
|
* How long the same weak box must keep being seen before it may re-anchor.
|
|
61364
61547
|
*
|
|
@@ -61538,6 +61721,7 @@ var StationaryObjectRegistry = class {
|
|
|
61538
61721
|
sessionGapMs;
|
|
61539
61722
|
warmUpMs;
|
|
61540
61723
|
ttlForDevice;
|
|
61724
|
+
onEntryDropped;
|
|
61541
61725
|
onChange;
|
|
61542
61726
|
requestConfirmationLook;
|
|
61543
61727
|
/**
|
|
@@ -61631,6 +61815,7 @@ var StationaryObjectRegistry = class {
|
|
|
61631
61815
|
this.sessionGapMs = deps.sessionGapMs ?? 5e3;
|
|
61632
61816
|
this.warmUpMs = deps.warmUpMs ?? 2e3;
|
|
61633
61817
|
this.ttlForDevice = deps.ttlForDevice;
|
|
61818
|
+
this.onEntryDropped = deps.onEntryDropped;
|
|
61634
61819
|
this.onChange = deps.onChange;
|
|
61635
61820
|
this.requestConfirmationLook = deps.requestConfirmationLook;
|
|
61636
61821
|
}
|
|
@@ -62240,6 +62425,7 @@ var StationaryObjectRegistry = class {
|
|
|
62240
62425
|
return at !== void 0 && at > entry.lastConfirmedAt;
|
|
62241
62426
|
}
|
|
62242
62427
|
dropEntry(deviceId, id) {
|
|
62428
|
+
const dropped = this.ledger.get(id);
|
|
62243
62429
|
this.idsByDevice.get(deviceId)?.delete(id);
|
|
62244
62430
|
this.dirty.delete(id);
|
|
62245
62431
|
this.weakRuns.delete(id);
|
|
@@ -62249,6 +62435,7 @@ var StationaryObjectRegistry = class {
|
|
|
62249
62435
|
if (armed.size === 0) this.departureStrikes.delete(deviceId);
|
|
62250
62436
|
}
|
|
62251
62437
|
this.ledger.forget(id);
|
|
62438
|
+
if (dropped !== void 0) this.onEntryDropped?.(dropped);
|
|
62252
62439
|
}
|
|
62253
62440
|
};
|
|
62254
62441
|
/** The persisted column map for an entry (the `id` PK is passed separately). */
|
|
@@ -68181,8 +68368,10 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
68181
68368
|
let liveTracks;
|
|
68182
68369
|
const liveEvents = /* @__PURE__ */ new Map();
|
|
68183
68370
|
let liveSummaries;
|
|
68371
|
+
let keptMedia = /* @__PURE__ */ new Set();
|
|
68184
68372
|
try {
|
|
68185
68373
|
liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
|
|
68374
|
+
if (deps.keptMediaKeys !== void 0 && scope.collection === "pipeline-analytics:media") keptMedia = await deps.keptMediaKeys(rows.map((row) => row.key));
|
|
68186
68375
|
for (const [table, ids] of eventIdsByTable) {
|
|
68187
68376
|
if (ids.length === 0) continue;
|
|
68188
68377
|
liveEvents.set(table, await deps.liveEventIds(table, ids));
|
|
@@ -68215,7 +68404,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
68215
68404
|
tally.unresolvable += 1;
|
|
68216
68405
|
continue;
|
|
68217
68406
|
}
|
|
68218
|
-
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;
|
|
68407
|
+
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;
|
|
68219
68408
|
const ts = num(row.data, scope.timestampField);
|
|
68220
68409
|
if (ts !== null && ts > graceFloor) {
|
|
68221
68410
|
tally.withinGrace += 1;
|
|
@@ -77949,10 +78138,149 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77949
78138
|
const last = track.positions.at(-1)?.bbox;
|
|
77950
78139
|
if (!last) return false;
|
|
77951
78140
|
return bboxMatchesParkedEntry(last, track.className, this.stationaryRegistry?.listViews(track.deviceId) ?? []);
|
|
77952
|
-
}
|
|
78141
|
+
},
|
|
78142
|
+
discardTracks: (tracks) => this.cascadeDiscardedTracks(tracks)
|
|
77953
78143
|
});
|
|
77954
78144
|
return this._trackCloser;
|
|
77955
78145
|
}
|
|
78146
|
+
/**
|
|
78147
|
+
* Cascade what `expireStale` refused to persist (D547): scenery re-fires and
|
|
78148
|
+
* re-sightings of a censused object. Per device, per track, through the
|
|
78149
|
+
* shipped registry — never a raw delete — so the vector, the crops and the
|
|
78150
|
+
* events go with the media. A track whose cascade fails is named.
|
|
78151
|
+
*/
|
|
78152
|
+
async cascadeDiscardedTracks(tracks) {
|
|
78153
|
+
const cascade = this.buildTrackCascadeRegistry();
|
|
78154
|
+
if (!cascade) {
|
|
78155
|
+
this.ctx.logger.warn("discarded tracks could not be cascaded — stores not ready", { meta: { trackIds: tracks.map((t) => t.trackId) } });
|
|
78156
|
+
return;
|
|
78157
|
+
}
|
|
78158
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
78159
|
+
for (const t of tracks) {
|
|
78160
|
+
const ids = byDevice.get(t.deviceId) ?? [];
|
|
78161
|
+
ids.push(t.trackId);
|
|
78162
|
+
byDevice.set(t.deviceId, ids);
|
|
78163
|
+
}
|
|
78164
|
+
for (const [deviceId, trackIds] of byDevice) {
|
|
78165
|
+
const result = await runTrackCascadeBatch({
|
|
78166
|
+
registry: cascade.registry,
|
|
78167
|
+
trackStore: cascade.trackStore,
|
|
78168
|
+
deviceId,
|
|
78169
|
+
onTrackCleanup: (id, trackId) => this.clearLiveTrackState(id, trackId),
|
|
78170
|
+
onFailure: (trackId, err) => {
|
|
78171
|
+
this.ctx.logger.warn("discarded track could not be cascaded", {
|
|
78172
|
+
tags: { deviceId },
|
|
78173
|
+
meta: {
|
|
78174
|
+
deviceId,
|
|
78175
|
+
trackId,
|
|
78176
|
+
error: String(err)
|
|
78177
|
+
}
|
|
78178
|
+
});
|
|
78179
|
+
}
|
|
78180
|
+
}, trackIds);
|
|
78181
|
+
this.ctx.logger.info("tracks discarded on expiry — still objects are entities, not tracks", {
|
|
78182
|
+
tags: { deviceId },
|
|
78183
|
+
meta: {
|
|
78184
|
+
deviceId,
|
|
78185
|
+
trackIds,
|
|
78186
|
+
deleted: result.deleted,
|
|
78187
|
+
failed: result.failed,
|
|
78188
|
+
mediaRows: result.tally.media
|
|
78189
|
+
}
|
|
78190
|
+
});
|
|
78191
|
+
}
|
|
78192
|
+
}
|
|
78193
|
+
/**
|
|
78194
|
+
* Dispose of the tracks that carried a promoted candidate's evidence (D547):
|
|
78195
|
+
* the one alive on the tipping frame and the persisted source of the entry's
|
|
78196
|
+
* best box. The entry's key frame is spared — it is the one row the entity
|
|
78197
|
+
* vouches for, and `keptStationaryMediaKeys` keeps the orphan audit off it.
|
|
78198
|
+
*/
|
|
78199
|
+
async disposePromotedTracks(deviceId, entry, liveTrackId, sourceTrackId) {
|
|
78200
|
+
const cascade = this.buildTrackCascadeRegistry();
|
|
78201
|
+
const mediaStore = this.mediaStore;
|
|
78202
|
+
if (!cascade || !mediaStore) {
|
|
78203
|
+
this.ctx.logger.warn("stationary promotion: source tracks could not be disposed of — stores not ready", {
|
|
78204
|
+
tags: { deviceId },
|
|
78205
|
+
meta: {
|
|
78206
|
+
deviceId,
|
|
78207
|
+
entryId: entry.id,
|
|
78208
|
+
liveTrackId,
|
|
78209
|
+
sourceTrackId: sourceTrackId ?? null
|
|
78210
|
+
}
|
|
78211
|
+
});
|
|
78212
|
+
return;
|
|
78213
|
+
}
|
|
78214
|
+
const plan = planStationaryTrackDisposal({
|
|
78215
|
+
liveTrackId,
|
|
78216
|
+
...sourceTrackId !== void 0 ? { sourceTrackId } : {},
|
|
78217
|
+
...entry.keyFrameMediaId !== void 0 ? { entryKeyFrameMediaId: entry.keyFrameMediaId } : {}
|
|
78218
|
+
});
|
|
78219
|
+
await disposeStationaryTracks({
|
|
78220
|
+
deviceId,
|
|
78221
|
+
entryId: entry.id,
|
|
78222
|
+
leaves: cascade.registry.filter((leaf) => leaf !== mediaStore),
|
|
78223
|
+
media: mediaStore,
|
|
78224
|
+
trackStore: cascade.trackStore,
|
|
78225
|
+
onTrackCleanup: (id, trackId) => this.clearLiveTrackState(id, trackId),
|
|
78226
|
+
logger: this.ctx.logger
|
|
78227
|
+
}, plan);
|
|
78228
|
+
}
|
|
78229
|
+
/**
|
|
78230
|
+
* Media row keys a live stationary entry vouches for (D547) — the orphan
|
|
78231
|
+
* audit's probe. Answered from the registry's RAM index, which is the
|
|
78232
|
+
* authority on which entries are alive.
|
|
78233
|
+
*/
|
|
78234
|
+
keptStationaryMediaKeys(keys) {
|
|
78235
|
+
const registry = this.stationaryRegistry;
|
|
78236
|
+
if (!registry) return /* @__PURE__ */ new Set();
|
|
78237
|
+
const held = /* @__PURE__ */ new Set();
|
|
78238
|
+
for (const deviceId of registry.deviceIds()) for (const entry of registry.list(deviceId)) if (entry.keyFrameMediaId !== void 0) held.add(String(entry.keyFrameMediaId));
|
|
78239
|
+
return new Set(keys.filter((key) => held.has(key)));
|
|
78240
|
+
}
|
|
78241
|
+
/**
|
|
78242
|
+
* An entry left the registry: free the key frame it vouched for (D547),
|
|
78243
|
+
* unless another live entry still holds it or a track row still owns it
|
|
78244
|
+
* (then retention's cascade will). Never silent either way.
|
|
78245
|
+
*/
|
|
78246
|
+
async releaseStationaryKeyFrame(entry) {
|
|
78247
|
+
const mediaId = entry.keyFrameMediaId;
|
|
78248
|
+
if (mediaId === void 0) return;
|
|
78249
|
+
const deviceId = entry.deviceId;
|
|
78250
|
+
const registry = this.stationaryRegistry;
|
|
78251
|
+
const mediaStore = this.mediaStore;
|
|
78252
|
+
const trackStore = this.trackStore;
|
|
78253
|
+
if (!registry || !mediaStore || !trackStore) return;
|
|
78254
|
+
if (registry.list(deviceId).some((e) => e.keyFrameMediaId === mediaId)) return;
|
|
78255
|
+
try {
|
|
78256
|
+
const owner = await mediaStore.ownerOf(mediaId);
|
|
78257
|
+
if (owner === null) return;
|
|
78258
|
+
if (owner.ownerKind === "track") {
|
|
78259
|
+
if ((await trackStore.existingTrackIds([owner.ownerId])).has(owner.ownerId)) return;
|
|
78260
|
+
}
|
|
78261
|
+
await mediaStore.deleteById(mediaId);
|
|
78262
|
+
this.ctx.logger.info("stationary entry released its key frame", {
|
|
78263
|
+
tags: { deviceId },
|
|
78264
|
+
meta: {
|
|
78265
|
+
deviceId,
|
|
78266
|
+
entryId: entry.id,
|
|
78267
|
+
mediaId,
|
|
78268
|
+
ownerKind: owner.ownerKind,
|
|
78269
|
+
ownerId: owner.ownerId
|
|
78270
|
+
}
|
|
78271
|
+
});
|
|
78272
|
+
} catch (err) {
|
|
78273
|
+
this.ctx.logger.warn("stationary entry key frame could not be released", {
|
|
78274
|
+
tags: { deviceId },
|
|
78275
|
+
meta: {
|
|
78276
|
+
deviceId,
|
|
78277
|
+
entryId: entry.id,
|
|
78278
|
+
mediaId,
|
|
78279
|
+
error: String(err)
|
|
78280
|
+
}
|
|
78281
|
+
});
|
|
78282
|
+
}
|
|
78283
|
+
}
|
|
77956
78284
|
/** Master toggle for the migrated object/face EmbeddingDispatcher. Read at
|
|
77957
78285
|
* init; a change takes effect on addon restart (same convention as
|
|
77958
78286
|
* `postProcessingNodeId`).
|
|
@@ -78678,6 +79006,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78678
79006
|
store: api.settingsStore,
|
|
78679
79007
|
logger: logger.child("StationaryRegistry"),
|
|
78680
79008
|
ttlForDevice: (deviceId) => this.stationarySettingsFromCache(deviceId).entryTtlMs,
|
|
79009
|
+
onEntryDropped: (entry) => {
|
|
79010
|
+
this.releaseStationaryKeyFrame(entry);
|
|
79011
|
+
},
|
|
78681
79012
|
requestConfirmationLook: (ask) => this.requestConfirmationLook(ask),
|
|
78682
79013
|
onChange: ({ phase, entry, timestamp, observedAt }) => {
|
|
78683
79014
|
this.ctx.eventBus.emit({
|
|
@@ -80066,6 +80397,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
80066
80397
|
anchor: { ...row.anchor },
|
|
80067
80398
|
agreed: row.agreed,
|
|
80068
80399
|
seen: row.seen,
|
|
80400
|
+
undecided: row.undecided,
|
|
80069
80401
|
ratio: row.seen > 0 ? row.agreed / row.seen : 0,
|
|
80070
80402
|
spanMs: row.lastAgreedAt - row.firstAgreedAt,
|
|
80071
80403
|
firstAgreedAt: row.firstAgreedAt,
|
|
@@ -81466,7 +81798,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
81466
81798
|
frameHeight: result.frameHeight,
|
|
81467
81799
|
...observedKeyFrame !== void 0 ? { keyFrameMediaId: observedKeyFrame } : {},
|
|
81468
81800
|
...observedLabel !== void 0 ? { label: observedLabel } : {},
|
|
81469
|
-
...movementState !== void 0 ? { movementState } : {}
|
|
81801
|
+
...movementState !== void 0 ? { movementState } : {},
|
|
81802
|
+
settling: t.settling
|
|
81470
81803
|
};
|
|
81471
81804
|
const outcome = evidence.observe(observation);
|
|
81472
81805
|
if (outcome === void 0 || !outcome.promoted) continue;
|
|
@@ -84616,17 +84949,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
84616
84949
|
wakeAssocIou: gate.wakeAssocIou
|
|
84617
84950
|
});
|
|
84618
84951
|
processor.dropTrack(track.trackId);
|
|
84619
|
-
this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
|
|
84620
|
-
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
84621
|
-
tags: { deviceId },
|
|
84622
|
-
meta: {
|
|
84623
|
-
deviceId,
|
|
84624
|
-
trackId: track.trackId,
|
|
84625
|
-
entryId: entry.id,
|
|
84626
|
-
error: String(err)
|
|
84627
|
-
}
|
|
84628
|
-
});
|
|
84629
|
-
});
|
|
84630
84952
|
const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, track.trackId);
|
|
84631
84953
|
const dropKeyFrame = () => {
|
|
84632
84954
|
this.residents.dropKeyFrameMediaId(track.trackId);
|
|
@@ -84634,7 +84956,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
84634
84956
|
if (faceEnd) faceEnd.finally(dropKeyFrame);
|
|
84635
84957
|
else dropKeyFrame();
|
|
84636
84958
|
const promotedPath = this.trackStore?.getActiveByTrack(track.trackId)?.positions;
|
|
84637
|
-
this.plateRecognizer?.onTrackEnd(deviceId, track.trackId, promotedPath);
|
|
84959
|
+
const plateEnd = this.plateRecognizer?.onTrackEnd(deviceId, track.trackId, promotedPath);
|
|
84960
|
+
Promise.allSettled([faceEnd, plateEnd]).then(() => this.disposePromotedTracks(deviceId, entry, track.trackId, row.sourceTrackId));
|
|
84638
84961
|
this.residents.close(track.trackId, "stationary");
|
|
84639
84962
|
this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
|
|
84640
84963
|
this.overlayState.onTrackEnded(deviceId, track.trackId);
|
|
@@ -85577,6 +85900,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
85577
85900
|
if (!summaries) throw new Error("summary store not built — probe unavailable");
|
|
85578
85901
|
return summaries.existingIds(ids);
|
|
85579
85902
|
},
|
|
85903
|
+
keptMediaKeys: async (keys) => this.keptStationaryMediaKeys(keys),
|
|
85580
85904
|
reclaim: buildOrphanReclaimer({
|
|
85581
85905
|
deleteFace: async (faceId) => {
|
|
85582
85906
|
await this.faceStore?.delete(faceId);
|