@camstack/addon-post-analysis 1.2.223 → 1.2.225
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-CxPGPwcy.mjs → dist-B-cXIX_e.mjs} +176 -11
- package/dist/{dist-iJA2F7_d.js → dist-BypQW8jz.js} +193 -10
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B3K65b9h.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CpDC7X_O.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-DoL6NTW5.mjs → hostInit-CchqWIpc.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +1158 -296
- package/dist/pipeline-analytics/index.mjs +1158 -296
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as TimelapseRuleSchema, $t as discriminatedUnion, A as NcConditionDescriptorSchema, At as readDeviceStateFrom, B as NcTaxonomySchema, Bt as zoneAnalyticsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as kebabToCamel, 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 nodePin, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as hydrateSchema, 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 boolean, R as NcSnoozeSuppressedSchema, Rt as vectorDimFromBase64, St as isSourceCap, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as notificationRulesCapability, 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 _enum, Y as SceneMonitorSchema, Yt as sleep$1, Z as TimelapseRuleInputSchema, Zt as array, _ as FULL_IMAGE_BBOX, _t as failureContributionCapability, a as CLUSTER_MODEL_SCOPED_STEPS, an as string, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isDetectionMacroClass, ct as cosineSimilarity$1, dt as deriveRecordingMode, en as literal, et as TrackSourceSchema, f as DeclaredDevices, g as FIRST_LEVEL_MACRO_CLASSES, gt as faceGalleryCapability, h as EVENT_PAD_MS, ht as evictionPolicyOfLocation, i as BirthDecisionRecordSchema, in as record, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readTimelapseGeneratedAt, k as NC_TAXONOMY, kt as plateGalleryCapability, lt as customAction, m as EVENT_OWNER_TYPES, mt as evaluateSensorEdge, n as ArchivedDebugNoteSchema, nn as object, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as unknown, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as encodeVectorBase64, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as isDeviceScopedCap, r as BaseDevice, rn as partialRecord, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as EventCategory, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as number, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, ut as defineCustomActions, v as FailureCounters, vt as formatMediaOwnerKey, w as MediaFileKindEnum, wt as mayWriteToLocation, x as MAX_BIRTH_DECISION_RECORDS, xt as isScheduleActive, y as LabelAttributionSchema, z as NcSystemEventKindSchema, zt as videoclipsCapability } from "../dist-B-cXIX_e.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";
|
|
@@ -10,6 +10,7 @@ import { startEventLoopStallMonitor } from "@camstack/system/addon-utils";
|
|
|
10
10
|
import sharp from "sharp";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
13
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
13
14
|
//#region src/pipeline-analytics/media-occupancy.ts
|
|
14
15
|
/** The bare type ref legacy NULL-stamped media rows belong to. */
|
|
15
16
|
var LEGACY_MEDIA_TYPE = "eventMedia";
|
|
@@ -49,6 +50,183 @@ function mediaOccupancyReports(footprint, locationIds, measuredAtMs) {
|
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
//#endregion
|
|
53
|
+
//#region src/pipeline-analytics/motion-episode.ts
|
|
54
|
+
/**
|
|
55
|
+
* Decide what one `(detected, timestamp)` observation does to the episode
|
|
56
|
+
* state for one `(deviceId, source)` key. `prior` is `undefined` the first
|
|
57
|
+
* time this key is ever observed (fresh process, or a key never seen).
|
|
58
|
+
*/
|
|
59
|
+
function decideMotionSignal(prior, detected, timestamp) {
|
|
60
|
+
if (!detected) {
|
|
61
|
+
if (prior === void 0) return { kind: "noop" };
|
|
62
|
+
return { kind: "observed-off" };
|
|
63
|
+
}
|
|
64
|
+
if (prior?.episode == null) return { kind: "open-episode" };
|
|
65
|
+
const isNewEdge = prior.lastDetected === false;
|
|
66
|
+
const edgeOffsetsMs = isNewEdge ? [...prior.episode.edgeOffsetsMs, timestamp - prior.episode.startedAt] : prior.episode.edgeOffsetsMs;
|
|
67
|
+
return {
|
|
68
|
+
kind: "extend-episode",
|
|
69
|
+
next: {
|
|
70
|
+
...prior.episode,
|
|
71
|
+
lastOnAt: timestamp,
|
|
72
|
+
edgeOffsetsMs
|
|
73
|
+
},
|
|
74
|
+
isNewEdge
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** `lastOnAt - startedAt` — see the module doc for why not `closedAt - startedAt`. */
|
|
78
|
+
function motionEpisodeDurationMs(state) {
|
|
79
|
+
return state.lastOnAt - state.startedAt;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/pipeline-analytics/motion-episode-tracker.ts
|
|
83
|
+
function keyOf(deviceId, source) {
|
|
84
|
+
return `${deviceId}:${source}`;
|
|
85
|
+
}
|
|
86
|
+
var MotionEpisodeTracker = class {
|
|
87
|
+
deps;
|
|
88
|
+
signals = /* @__PURE__ */ new Map();
|
|
89
|
+
timers = /* @__PURE__ */ new Map();
|
|
90
|
+
constructor(deps) {
|
|
91
|
+
this.deps = deps;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* One `(detected, timestamp)` observation for one `(deviceId, source)` key.
|
|
95
|
+
* `regionData` is read only if this observation OPENS a new episode — see
|
|
96
|
+
* {@link MotionEpisodeInsertInput}.
|
|
97
|
+
*/
|
|
98
|
+
async observe(deviceId, source, detected, timestamp, regionData) {
|
|
99
|
+
const key = keyOf(deviceId, source);
|
|
100
|
+
const prior = this.signals.get(key);
|
|
101
|
+
const decision = decideMotionSignal(prior, detected, timestamp);
|
|
102
|
+
switch (decision.kind) {
|
|
103
|
+
case "noop": return;
|
|
104
|
+
case "observed-off":
|
|
105
|
+
this.setSignal(key, deviceId, source, false, prior?.episode ?? null);
|
|
106
|
+
return;
|
|
107
|
+
case "open-episode": {
|
|
108
|
+
const eventId = await this.deps.insertRow({
|
|
109
|
+
deviceId,
|
|
110
|
+
source,
|
|
111
|
+
timestamp,
|
|
112
|
+
regionData
|
|
113
|
+
});
|
|
114
|
+
if (eventId === null) {
|
|
115
|
+
this.deps.logger.warn("motion episode open failed — insert did not land", {
|
|
116
|
+
tags: { deviceId },
|
|
117
|
+
meta: {
|
|
118
|
+
source,
|
|
119
|
+
timestamp
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
this.setSignal(key, deviceId, source, true, null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const episode = {
|
|
126
|
+
eventId,
|
|
127
|
+
startedAt: timestamp,
|
|
128
|
+
lastOnAt: timestamp,
|
|
129
|
+
edgeOffsetsMs: [0]
|
|
130
|
+
};
|
|
131
|
+
this.setSignal(key, deviceId, source, true, episode);
|
|
132
|
+
this.armCloseTimer(key);
|
|
133
|
+
this.deps.onNewEdge?.({
|
|
134
|
+
deviceId,
|
|
135
|
+
source,
|
|
136
|
+
timestamp,
|
|
137
|
+
eventId,
|
|
138
|
+
isFirst: true
|
|
139
|
+
});
|
|
140
|
+
this.deps.onTick?.({
|
|
141
|
+
deviceId,
|
|
142
|
+
source,
|
|
143
|
+
timestamp
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
case "extend-episode":
|
|
148
|
+
this.setSignal(key, deviceId, source, true, decision.next);
|
|
149
|
+
this.armCloseTimer(key);
|
|
150
|
+
if (decision.isNewEdge) this.deps.onNewEdge?.({
|
|
151
|
+
deviceId,
|
|
152
|
+
source,
|
|
153
|
+
timestamp,
|
|
154
|
+
eventId: decision.next.eventId,
|
|
155
|
+
isFirst: false
|
|
156
|
+
});
|
|
157
|
+
this.deps.onTick?.({
|
|
158
|
+
deviceId,
|
|
159
|
+
source,
|
|
160
|
+
timestamp
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Close every currently-open episode immediately, without waiting out the
|
|
167
|
+
* quiet window — used at addon shutdown so a row never sits with
|
|
168
|
+
* `durationMs: null` forever because the process that would have closed it
|
|
169
|
+
* is gone. Each close is logged with `reason` so an operator reading the
|
|
170
|
+
* row's neighbours understands why it stopped short of a natural close.
|
|
171
|
+
*/
|
|
172
|
+
async closeAll(reason) {
|
|
173
|
+
const keys = [...this.signals.keys()];
|
|
174
|
+
for (const key of keys) {
|
|
175
|
+
const signal = this.signals.get(key);
|
|
176
|
+
if (!signal?.episode) continue;
|
|
177
|
+
await this.closeNow(key, signal, reason);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
setSignal(key, deviceId, source, lastDetected, episode) {
|
|
181
|
+
this.signals.set(key, {
|
|
182
|
+
deviceId,
|
|
183
|
+
source,
|
|
184
|
+
lastDetected,
|
|
185
|
+
episode
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
armCloseTimer(key) {
|
|
189
|
+
const existing = this.timers.get(key);
|
|
190
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
191
|
+
const timer = setTimeout(() => {
|
|
192
|
+
this.fireClose(key);
|
|
193
|
+
}, this.deps.closeAfterMs);
|
|
194
|
+
timer.unref?.();
|
|
195
|
+
this.timers.set(key, timer);
|
|
196
|
+
}
|
|
197
|
+
async fireClose(key) {
|
|
198
|
+
this.timers.delete(key);
|
|
199
|
+
const signal = this.signals.get(key);
|
|
200
|
+
if (!signal?.episode) return;
|
|
201
|
+
await this.closeNow(key, signal, "quiet-window-elapsed");
|
|
202
|
+
}
|
|
203
|
+
async closeNow(key, signal, reason) {
|
|
204
|
+
const episode = signal.episode;
|
|
205
|
+
if (!episode) return;
|
|
206
|
+
const timer = this.timers.get(key);
|
|
207
|
+
if (timer !== void 0) {
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
this.timers.delete(key);
|
|
210
|
+
}
|
|
211
|
+
signal.episode = null;
|
|
212
|
+
const durationMs = motionEpisodeDurationMs(episode);
|
|
213
|
+
if (!await this.deps.closeRow({
|
|
214
|
+
deviceId: signal.deviceId,
|
|
215
|
+
source: signal.source,
|
|
216
|
+
eventId: episode.eventId,
|
|
217
|
+
durationMs,
|
|
218
|
+
edges: episode.edgeOffsetsMs
|
|
219
|
+
})) this.deps.logger.warn("motion episode close failed — row keeps durationMs: null", {
|
|
220
|
+
tags: { deviceId: signal.deviceId },
|
|
221
|
+
meta: {
|
|
222
|
+
source: signal.source,
|
|
223
|
+
eventId: episode.eventId,
|
|
224
|
+
reason
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
//#endregion
|
|
52
230
|
//#region src/notification-center/action-token.ts
|
|
53
231
|
/**
|
|
54
232
|
* The authority behind a notification button.
|
|
@@ -6750,7 +6928,7 @@ function normalizeBbox(bbox, frameWidth, frameHeight) {
|
|
|
6750
6928
|
function subjectFromObjectEvent(ev) {
|
|
6751
6929
|
return {
|
|
6752
6930
|
kind: "object-event",
|
|
6753
|
-
recordId: ev.id,
|
|
6931
|
+
recordId: String(ev.id),
|
|
6754
6932
|
deviceId: ev.deviceId,
|
|
6755
6933
|
timestamp: ev.timestamp,
|
|
6756
6934
|
classNames: [ev.className],
|
|
@@ -6818,7 +6996,7 @@ function subjectFromSensorEvent(ev, markerTrackId) {
|
|
|
6818
6996
|
const eventType = readSensorEventType(ev.value);
|
|
6819
6997
|
return {
|
|
6820
6998
|
kind: "device-event",
|
|
6821
|
-
recordId: ev.id,
|
|
6999
|
+
recordId: String(ev.id),
|
|
6822
7000
|
deviceId: ev.deviceId,
|
|
6823
7001
|
...markerTrackId !== void 0 ? { trackId: markerTrackId } : {},
|
|
6824
7002
|
...ev.sourceDeviceId !== ev.deviceId ? { sourceDeviceId: ev.sourceDeviceId } : {},
|
|
@@ -6855,7 +7033,7 @@ function subjectFromAudioEvent(ev, stillOwnerId) {
|
|
|
6855
7033
|
const macro = ev.classification?.className;
|
|
6856
7034
|
return {
|
|
6857
7035
|
kind: "audio-event",
|
|
6858
|
-
recordId: ev.id,
|
|
7036
|
+
recordId: String(ev.id),
|
|
6859
7037
|
deviceId: ev.deviceId,
|
|
6860
7038
|
timestamp: ev.timestamp,
|
|
6861
7039
|
classNames: macro !== void 0 ? [`audio-${macro}`] : [],
|
|
@@ -6985,7 +7163,7 @@ function occupancySubjectFromEdge(edge) {
|
|
|
6985
7163
|
function subjectFromPackageEvent(ev, phase) {
|
|
6986
7164
|
return {
|
|
6987
7165
|
kind: "package-event",
|
|
6988
|
-
recordId: ev.id,
|
|
7166
|
+
recordId: String(ev.id),
|
|
6989
7167
|
deviceId: ev.deviceId,
|
|
6990
7168
|
timestamp: ev.timestamp,
|
|
6991
7169
|
classNames: [ev.className],
|
|
@@ -11667,7 +11845,7 @@ var NcDispatcher = class {
|
|
|
11667
11845
|
recordId: entry.recordId,
|
|
11668
11846
|
policy: entry.payload.media,
|
|
11669
11847
|
...entry.payload.mediaFrame !== void 0 ? { frame: entry.payload.mediaFrame } : {},
|
|
11670
|
-
owners: [...entry.payload.subject.eventId !== void 0 ? [`
|
|
11848
|
+
owners: [...entry.payload.subject.eventId !== void 0 ? [`object:${entry.payload.subject.eventId}`] : [], ...entry.payload.subject.trackId !== void 0 ? [`track:${entry.payload.subject.trackId}`] : []]
|
|
11671
11849
|
}
|
|
11672
11850
|
});
|
|
11673
11851
|
out.push(...footage.attachments);
|
|
@@ -11974,7 +12152,7 @@ var NcDispatcher = class {
|
|
|
11974
12152
|
const signal = policy === "best-matching" ? matchSignal(entry.payload.matchedOn) : null;
|
|
11975
12153
|
const owners = [];
|
|
11976
12154
|
if (subject.eventId !== void 0) owners.push({
|
|
11977
|
-
kind: "
|
|
12155
|
+
kind: "object",
|
|
11978
12156
|
id: subject.eventId
|
|
11979
12157
|
});
|
|
11980
12158
|
if (subject.trackId !== void 0) owners.push({
|
|
@@ -28194,16 +28372,28 @@ var StoragePressureTracker = class {
|
|
|
28194
28372
|
* operator-captured reference picture as collectable. It is the same class of
|
|
28195
28373
|
* mistake `identity` and `vehicle` are exempt for: a blob the OPERATOR curated,
|
|
28196
28374
|
* which nothing else can regenerate and no cascade owns.
|
|
28375
|
+
*
|
|
28376
|
+
* `event` was DROPPED and replaced by `motion` / `object` / `audio` (D476).
|
|
28377
|
+
* `event` used to cover all three event tables under one owner kind, which was
|
|
28378
|
+
* safe while event ids were UUIDs (unique across tables). D474 made an event
|
|
28379
|
+
* id a per-table SQLite rowid alias, so `event:5` stopped naming one row —
|
|
28380
|
+
* three different rows, one per table, can all be `5`. This is the AUTHORITY
|
|
28381
|
+
* on what may own media; `MEDIA_OWNER_TYPES` in
|
|
28382
|
+
* `@camstack/types/media/media-owner-key.ts` must cover every member here —
|
|
28383
|
+
* `scripts` / `__tests__/owner-kinds-authority.spec.ts` guards the two lists
|
|
28384
|
+
* staying married.
|
|
28197
28385
|
*/
|
|
28198
28386
|
var OWNER_KINDS = [
|
|
28199
|
-
"event",
|
|
28200
28387
|
"track",
|
|
28201
28388
|
"summary",
|
|
28202
28389
|
"face",
|
|
28203
28390
|
"identity",
|
|
28204
28391
|
"plate",
|
|
28205
28392
|
"vehicle",
|
|
28206
|
-
"scene"
|
|
28393
|
+
"scene",
|
|
28394
|
+
"motion",
|
|
28395
|
+
"object",
|
|
28396
|
+
"audio"
|
|
28207
28397
|
];
|
|
28208
28398
|
/**
|
|
28209
28399
|
* Owner kinds that NOTHING may write any more, but whose rows can still be on
|
|
@@ -28600,7 +28790,12 @@ function mediaRowLocation(data) {
|
|
|
28600
28790
|
return typeof id === "string" && id.length > 0 ? id : DEFAULT_MEDIA_LOCATION;
|
|
28601
28791
|
}
|
|
28602
28792
|
function buildKey(params) {
|
|
28603
|
-
return
|
|
28793
|
+
return formatMediaOwnerKey({
|
|
28794
|
+
ownerType: params.ownerKind,
|
|
28795
|
+
ownerId: params.ownerId,
|
|
28796
|
+
fileKind: params.kind,
|
|
28797
|
+
...isSingleInstanceKind(params.kind) ? {} : { timestampMs: params.timestamp }
|
|
28798
|
+
});
|
|
28604
28799
|
}
|
|
28605
28800
|
function buildPath(params) {
|
|
28606
28801
|
const base = `${params.deviceId}/events/${params.ownerKind}/${params.ownerId}`;
|
|
@@ -29515,9 +29710,27 @@ var MediaStore = class {
|
|
|
29515
29710
|
}
|
|
29516
29711
|
return newKey;
|
|
29517
29712
|
}
|
|
29518
|
-
/**
|
|
29519
|
-
|
|
29520
|
-
|
|
29713
|
+
/**
|
|
29714
|
+
* Delete event-owned media for evicted events, GROUPED by the table each id
|
|
29715
|
+
* came from (D476).
|
|
29716
|
+
*
|
|
29717
|
+
* There is no honest single-kind signature any more: `deleteForOwner` needs
|
|
29718
|
+
* ONE owner kind per call, and a flat id list can no longer say which of
|
|
29719
|
+
* `motion` / `object` / `audio` an id names — D474 made event ids per-table
|
|
29720
|
+
* rowids, so `motion:5` and `object:5` are different rows that both spell
|
|
29721
|
+
* `5`. Every caller (the retention cascade's per-track object-event sweep,
|
|
29722
|
+
* the trackless motion/audio sweep, the operator prune) already KNOWS which
|
|
29723
|
+
* table produced each id before it reaches here — the old flat signature
|
|
29724
|
+
* just threw that fact away. `ids` is a partial record so a caller supplies
|
|
29725
|
+
* only the kinds it actually collected.
|
|
29726
|
+
*/
|
|
29727
|
+
async deleteForEvents(ids) {
|
|
29728
|
+
let total = 0;
|
|
29729
|
+
for (const ownerType of EVENT_OWNER_TYPES) {
|
|
29730
|
+
const kindIds = ids[ownerType];
|
|
29731
|
+
if (kindIds !== void 0 && kindIds.length > 0) total += await this.deleteForOwner(ownerType, kindIds);
|
|
29732
|
+
}
|
|
29733
|
+
return total;
|
|
29521
29734
|
}
|
|
29522
29735
|
/** Delete all media owned by these tracks (keyFrame/snapshot/thumbnail). */
|
|
29523
29736
|
async deleteForTracks(trackIds) {
|
|
@@ -29532,8 +29745,9 @@ var MediaStore = class {
|
|
|
29532
29745
|
* ENROLLED face/plate has its crop REOWNED to `identity`/`vehicle` at
|
|
29533
29746
|
* assignment, so deleting only the `'face'`/`'plate'` owner removes unassigned
|
|
29534
29747
|
* crops and leaves the enrolled gallery intact. Event-owned media
|
|
29535
|
-
* (`ownerKind:'
|
|
29536
|
-
*
|
|
29748
|
+
* (`ownerKind:'motion'` / `'object'` / `'audio'`) is handled by `EventStore`
|
|
29749
|
+
* (which holds the per-kind event id sets — see {@link deleteForEvents}).
|
|
29750
|
+
* `deleteForOwner` is best-effort per row, so one bad row can't abort the
|
|
29537
29751
|
* batch.
|
|
29538
29752
|
*/
|
|
29539
29753
|
async deleteByTracks(trackIds) {
|
|
@@ -36396,23 +36610,6 @@ function ruleApplies(resolved, det, className, maskInfo, _zones, frameWidth, fra
|
|
|
36396
36610
|
}
|
|
36397
36611
|
//#endregion
|
|
36398
36612
|
//#region src/pipeline-analytics/pipeline/frame-processor.ts
|
|
36399
|
-
/**
|
|
36400
|
-
* FrameProcessor — per-device pipeline stage runner.
|
|
36401
|
-
*
|
|
36402
|
-
* Consumes a raw `PipelineInferenceResult` payload and runs:
|
|
36403
|
-
* 1. Flatten first-level detections
|
|
36404
|
-
* 2. Zone filter (ZoneEngine)
|
|
36405
|
-
* 3. Tracking (SortTracker)
|
|
36406
|
-
* 4. State analysis (StateAnalyzer)
|
|
36407
|
-
* 5. Event emission (DetectionEventEmitter)
|
|
36408
|
-
*
|
|
36409
|
-
* Returns a structured result the addon uses to:
|
|
36410
|
-
* - upsert TrackStore
|
|
36411
|
-
* - insert ObjectEvent rows into the declared collection
|
|
36412
|
-
* - emit onFrameTracked / onDetectionEvent on the bus
|
|
36413
|
-
*
|
|
36414
|
-
* One FrameProcessor per device — lifecycle managed by DeviceRegistry.
|
|
36415
|
-
*/
|
|
36416
36613
|
/** Shared empty — a fold is rare and every other frame would allocate. */
|
|
36417
36614
|
var EMPTY_RIDER_FOLDS = [];
|
|
36418
36615
|
/** Mapping from StateAnalyzer's `ObjectState.state` values to the
|
|
@@ -37148,7 +37345,6 @@ var FrameProcessor = class {
|
|
|
37148
37345
|
const crossing = crossingOf(e);
|
|
37149
37346
|
const labelPatch = labelPatchByTrack.get(e.detection.trackId) ?? knownTrackLabelPatch(this.trackLabelState?.(e.detection.trackId));
|
|
37150
37347
|
return {
|
|
37151
|
-
id: randomUUID(),
|
|
37152
37348
|
deviceId: this.deviceId,
|
|
37153
37349
|
timestamp,
|
|
37154
37350
|
kind: "object",
|
|
@@ -37885,7 +38081,7 @@ async function cascadeDeleteTrack(stores, trackId) {
|
|
|
37885
38081
|
for (const id of ids) {
|
|
37886
38082
|
const eventIds = await stores.eventStore.deleteByTrack(id);
|
|
37887
38083
|
events += eventIds.length;
|
|
37888
|
-
if (eventIds.length > 0) media += await stores.mediaStore.deleteForEvents([...eventIds]);
|
|
38084
|
+
if (eventIds.length > 0) media += await stores.mediaStore.deleteForEvents({ object: [...eventIds] });
|
|
37889
38085
|
}
|
|
37890
38086
|
return [{
|
|
37891
38087
|
family: "events",
|
|
@@ -39842,6 +40038,11 @@ function runFrameProcessorPass(input) {
|
|
|
39842
40038
|
* remaining blob are dropped — including snapshot-less rows. Enrolled gallery
|
|
39843
40039
|
* media (`identity` / `vehicle` / `scene`) is never probed.
|
|
39844
40040
|
*/
|
|
40041
|
+
/** Is this an event-owned media row — `motion` / `object` / `audio` (D476;
|
|
40042
|
+
* `event` used to cover all three under one owner kind). */
|
|
40043
|
+
function isEventOwnedMediaRow(ownerKind) {
|
|
40044
|
+
return EVENT_OWNER_TYPES.includes(ownerKind);
|
|
40045
|
+
}
|
|
39845
40046
|
function shouldProbeMediaRow(ownerKind) {
|
|
39846
40047
|
if (!isMediaOwnerKind(ownerKind)) return true;
|
|
39847
40048
|
return !isRetentionExemptOwnerKind(ownerKind);
|
|
@@ -39872,7 +40073,7 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
39872
40073
|
if (row.path.length === 0 ? false : await deps.blobExists(row.location, row.path)) {
|
|
39873
40074
|
const trackId = trackIdFromMediaOwner(row.ownerKind, row.ownerId);
|
|
39874
40075
|
if (trackId !== null) tracksWithMedia.add(trackId);
|
|
39875
|
-
if (row.ownerKind
|
|
40076
|
+
if (isEventOwnedMediaRow(row.ownerKind)) eventsWithMedia.add(row.ownerId);
|
|
39876
40077
|
continue;
|
|
39877
40078
|
}
|
|
39878
40079
|
missingKeys.push(row.key);
|
|
@@ -39933,6 +40134,191 @@ var locationDiscardActions = defineCustomActions({
|
|
|
39933
40134
|
"storage.discardLocationCancel": customAction(object({}), DiscardCancel, { auth: "admin" })
|
|
39934
40135
|
});
|
|
39935
40136
|
//#endregion
|
|
40137
|
+
//#region src/pipeline-analytics/store/motion-edge-codec.ts
|
|
40138
|
+
/**
|
|
40139
|
+
* How a motion episode's rising-edge offsets become bytes, and back (D475).
|
|
40140
|
+
*
|
|
40141
|
+
* A motion episode row (`durationMs` + `edges`) records every distinct
|
|
40142
|
+
* off→on transition the camera reported while the episode stayed open — "ogni
|
|
40143
|
+
* evento on si deve salvare" (the operator's words). `edges` holds those
|
|
40144
|
+
* instants as **ms offsets from the episode's `timestamp`** (its first rising
|
|
40145
|
+
* edge, so the first entry is always `0`), never as absolute epochs — an
|
|
40146
|
+
* epoch is ~13 digits per entry; an offset into a 40 s episode is 1–5.
|
|
40147
|
+
*
|
|
40148
|
+
* Mirrors `position-codec.ts` (D471) on purpose: same discipline, different
|
|
40149
|
+
* shape. A MALFORMED stored value decodes to an empty array, never a throw —
|
|
40150
|
+
* one row's garbage `edges` column must not take out the whole event read
|
|
40151
|
+
* path, and a legacy row (written before this column existed) has no `edges`
|
|
40152
|
+
* at all, which decodes to `[]` exactly like garbage does. The open/closed
|
|
40153
|
+
* question is answered by `durationMs` being `null` vs a number vs absent —
|
|
40154
|
+
* never by this column, so an empty `edges` array must never be read as "no
|
|
40155
|
+
* episode" or "still open".
|
|
40156
|
+
*/
|
|
40157
|
+
/**
|
|
40158
|
+
* Marks a value this module wrote. A distinct namespace from
|
|
40159
|
+
* `position-codec.ts`'s `z1:` — different column, different shape, so a
|
|
40160
|
+
* value from one can never be mistaken for the other's.
|
|
40161
|
+
*/
|
|
40162
|
+
var ENCODED_PREFIX$1 = "me1:";
|
|
40163
|
+
/** Round-trip a sequence of rising-edge offsets (ms, from the episode start)
|
|
40164
|
+
* through gzip + base64 — the same trade `position-codec.ts` made: base64
|
|
40165
|
+
* because the settings-store column types have no BLOB, gzip because the
|
|
40166
|
+
* repeated small integers compress well. */
|
|
40167
|
+
function encodeMotionEdges(edgesMs) {
|
|
40168
|
+
const json = JSON.stringify(edgesMs);
|
|
40169
|
+
return ENCODED_PREFIX$1 + gzipSync(Buffer.from(json, "utf8"), { level: 6 }).toString("base64");
|
|
40170
|
+
}
|
|
40171
|
+
/**
|
|
40172
|
+
* Read a stored `edges` value. Two shapes reach here: a `me1:` string this
|
|
40173
|
+
* module wrote, and a plain array (the store parsed the JSON column for us,
|
|
40174
|
+
* or a caller handed one straight through — the codec should not be the
|
|
40175
|
+
* reason a valid in-memory array fails to read back). Anything else —
|
|
40176
|
+
* `undefined` (legacy row, or an episode that never closed), a garbage
|
|
40177
|
+
* string, a number — answers `[]`.
|
|
40178
|
+
*/
|
|
40179
|
+
function decodeMotionEdges(value) {
|
|
40180
|
+
if (Array.isArray(value)) return value.filter(isFiniteNumber);
|
|
40181
|
+
if (typeof value !== "string") return [];
|
|
40182
|
+
if (value.startsWith(ENCODED_PREFIX$1)) try {
|
|
40183
|
+
const raw = gunzipSync(Buffer.from(value.slice(4), "base64")).toString("utf8");
|
|
40184
|
+
const parsed = JSON.parse(raw);
|
|
40185
|
+
return Array.isArray(parsed) ? parsed.filter(isFiniteNumber) : [];
|
|
40186
|
+
} catch {
|
|
40187
|
+
return [];
|
|
40188
|
+
}
|
|
40189
|
+
try {
|
|
40190
|
+
const parsed = JSON.parse(value);
|
|
40191
|
+
return Array.isArray(parsed) ? parsed.filter(isFiniteNumber) : [];
|
|
40192
|
+
} catch {
|
|
40193
|
+
return [];
|
|
40194
|
+
}
|
|
40195
|
+
}
|
|
40196
|
+
function isFiniteNumber(v) {
|
|
40197
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
40198
|
+
}
|
|
40199
|
+
//#endregion
|
|
40200
|
+
//#region src/pipeline-analytics/store/position-codec.ts
|
|
40201
|
+
/**
|
|
40202
|
+
* How a track's `positions[]` becomes bytes, and back (D471).
|
|
40203
|
+
*
|
|
40204
|
+
* `positions` was 94.9 % of the `tracks` table and 17 % of the whole settings
|
|
40205
|
+
* database — 186.5 MB — and most of that was not information. See
|
|
40206
|
+
* `__tests__/position-codec.spec.ts` for the measurement and for why rounding
|
|
40207
|
+
* is safe only since D399.
|
|
40208
|
+
*
|
|
40209
|
+
* Pure on purpose: what a MALFORMED stored value decodes to is the decision
|
|
40210
|
+
* worth pinning in a test, and it is "an empty trail", never a throw.
|
|
40211
|
+
*/
|
|
40212
|
+
/**
|
|
40213
|
+
* Marks a value this module wrote. Chosen so it cannot collide with the plain
|
|
40214
|
+
* JSON array every pre-D471 row holds: that always starts `[`.
|
|
40215
|
+
*/
|
|
40216
|
+
var ENCODED_PREFIX = "z1:";
|
|
40217
|
+
/** Pixels are integers. The float tail is representation noise, not precision. */
|
|
40218
|
+
function roundPosition(p) {
|
|
40219
|
+
return {
|
|
40220
|
+
...p,
|
|
40221
|
+
x: Math.round(p.x),
|
|
40222
|
+
y: Math.round(p.y),
|
|
40223
|
+
bbox: {
|
|
40224
|
+
...p.bbox,
|
|
40225
|
+
x: Math.round(p.bbox.x),
|
|
40226
|
+
y: Math.round(p.bbox.y),
|
|
40227
|
+
w: Math.round(p.bbox.w),
|
|
40228
|
+
h: Math.round(p.bbox.h)
|
|
40229
|
+
}
|
|
40230
|
+
};
|
|
40231
|
+
}
|
|
40232
|
+
/** Round, serialise, gzip, base64 — in that order, so the compressor sees the
|
|
40233
|
+
* smaller text and the repeated field names it compresses best. */
|
|
40234
|
+
function encodePositions(positions) {
|
|
40235
|
+
const json = JSON.stringify(positions.map(roundPosition));
|
|
40236
|
+
return ENCODED_PREFIX + gzipSync(Buffer.from(json, "utf8"), { level: 6 }).toString("base64");
|
|
40237
|
+
}
|
|
40238
|
+
/**
|
|
40239
|
+
* Read a stored value, whichever era wrote it.
|
|
40240
|
+
*
|
|
40241
|
+
* THREE shapes reach here and all three are ordinary, which is what makes a
|
|
40242
|
+
* migration unnecessary: a `z1:` string this module wrote, a plain array (the
|
|
40243
|
+
* store parsed the JSON column for us), and a plain JSON string (it did not).
|
|
40244
|
+
* Anything else is a row we cannot read, and the answer is an empty trail —
|
|
40245
|
+
* the timeline then draws a track with no path, which an operator can SEE,
|
|
40246
|
+
* instead of failing the page, which they cannot.
|
|
40247
|
+
*/
|
|
40248
|
+
function decodePositions(value) {
|
|
40249
|
+
if (Array.isArray(value)) return value.filter(isPosition);
|
|
40250
|
+
if (typeof value !== "string") return [];
|
|
40251
|
+
if (value.startsWith(ENCODED_PREFIX)) try {
|
|
40252
|
+
const raw = gunzipSync(Buffer.from(value.slice(3), "base64")).toString("utf8");
|
|
40253
|
+
const parsed = JSON.parse(raw);
|
|
40254
|
+
return Array.isArray(parsed) ? parsed.filter(isPosition) : [];
|
|
40255
|
+
} catch {
|
|
40256
|
+
return [];
|
|
40257
|
+
}
|
|
40258
|
+
try {
|
|
40259
|
+
const parsed = JSON.parse(value);
|
|
40260
|
+
return Array.isArray(parsed) ? parsed.filter(isPosition) : [];
|
|
40261
|
+
} catch {
|
|
40262
|
+
return [];
|
|
40263
|
+
}
|
|
40264
|
+
}
|
|
40265
|
+
function isPosition(v) {
|
|
40266
|
+
if (typeof v !== "object" || v === null) return false;
|
|
40267
|
+
const p = { ...v };
|
|
40268
|
+
return typeof p["x"] === "number" && typeof p["y"] === "number" && typeof p["bbox"] === "object";
|
|
40269
|
+
}
|
|
40270
|
+
/**
|
|
40271
|
+
* A trajectory reduced to the ONLY thing a zone-filtered reader uses (D471).
|
|
40272
|
+
*
|
|
40273
|
+
* The client's `parsePositionBoxes` walks `positions[]` and keeps `p.bbox`,
|
|
40274
|
+
* discarding `x`, `y`, `timestamp` and `source` the moment they arrive. On the
|
|
40275
|
+
* zone path — the one path that ships positions at all — that is **59 % of the
|
|
40276
|
+
* payload thrown away by the receiver**: 162 B per position sent, 66 B used.
|
|
40277
|
+
*
|
|
40278
|
+
* Measured on 20 130 real positions. This is the reduction, applied server-side
|
|
40279
|
+
* so the bytes are never spent.
|
|
40280
|
+
*
|
|
40281
|
+
* NOT applied to any other read: a caller that asked for a full trajectory gets
|
|
40282
|
+
* one. This is narrowing on the request that already narrowed.
|
|
40283
|
+
*/
|
|
40284
|
+
function geometryOnlyPositions(positions) {
|
|
40285
|
+
return positions.map((p) => ({
|
|
40286
|
+
x: p.bbox.x,
|
|
40287
|
+
y: p.bbox.y,
|
|
40288
|
+
timestamp: 0,
|
|
40289
|
+
bbox: p.bbox
|
|
40290
|
+
}));
|
|
40291
|
+
}
|
|
40292
|
+
/**
|
|
40293
|
+
* A detection box with integer pixel coordinates (D471).
|
|
40294
|
+
*
|
|
40295
|
+
* `object-events.bbox` carries the same float tail as `positions` did —
|
|
40296
|
+
* `{"w":1089.6000000000001,"h":398.40000000000003}` — and is 18 % of a 30 MB
|
|
40297
|
+
* table. Same cause: a pixel scaled by the analysis raster ratio, serialised
|
|
40298
|
+
* with every digit JavaScript can print.
|
|
40299
|
+
*
|
|
40300
|
+
* Returns the input unchanged when there is nothing to round, so a legacy row
|
|
40301
|
+
* with no bbox stays exactly what it was.
|
|
40302
|
+
*/
|
|
40303
|
+
function roundBbox(bbox) {
|
|
40304
|
+
if (typeof bbox !== "object" || bbox === null) return bbox;
|
|
40305
|
+
const b = { ...bbox };
|
|
40306
|
+
let touched = false;
|
|
40307
|
+
for (const k of [
|
|
40308
|
+
"x",
|
|
40309
|
+
"y",
|
|
40310
|
+
"w",
|
|
40311
|
+
"h"
|
|
40312
|
+
]) {
|
|
40313
|
+
const v = b[k];
|
|
40314
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
40315
|
+
b[k] = Math.round(v);
|
|
40316
|
+
touched = true;
|
|
40317
|
+
}
|
|
40318
|
+
}
|
|
40319
|
+
return touched ? b : bbox;
|
|
40320
|
+
}
|
|
40321
|
+
//#endregion
|
|
39936
40322
|
//#region src/pipeline-analytics/store/tiered-label-columns.ts
|
|
39937
40323
|
/**
|
|
39938
40324
|
* The six tiered-label columns, read and written in ONE place (roadmap 4g).
|
|
@@ -40053,12 +40439,17 @@ function audioDbfsByBucket(events, since, bucketMs) {
|
|
|
40053
40439
|
//#region src/pipeline-analytics/store/event-store.ts
|
|
40054
40440
|
/**
|
|
40055
40441
|
* @durable class=ledger owner=pipeline-analytics
|
|
40056
|
-
* write="one row
|
|
40057
|
-
*
|
|
40058
|
-
*
|
|
40059
|
-
*
|
|
40442
|
+
* write="one row per motion EPISODE, keyed per (deviceId, source) so the
|
|
40443
|
+
* analyzer and a firmware/device-activity sensor on the SAME camera never
|
|
40444
|
+
* share one episode (D475). INSERT on a rising edge with no episode open
|
|
40445
|
+
* (durationMs: null, edges: [0]); a further rising edge inside the open
|
|
40446
|
+
* episode does NOT insert — it extends the in-memory episode and re-arms
|
|
40447
|
+
* the MOTION_CLOSE_AFTER_MS close timer. One UPDATE at close writes the
|
|
40448
|
+
* final durationMs (lastOnAt - startedAt) and the encoded edges array.
|
|
40449
|
+
* Replaces the pre-D475 scheme (one row off→on, then a heartbeat row
|
|
40450
|
+
* every 5 s while motion stayed on) — MOTION_EVENT_HEARTBEAT_MS is gone."
|
|
40060
40451
|
* retention="a TERMINAL age-keyed root — no trackId. A motion row may own a
|
|
40061
|
-
* still (`ownerKind: '
|
|
40452
|
+
* still (`ownerKind: 'motion'`, `kind: 'snapshot'`) captured on the off→on
|
|
40062
40453
|
* visit so events-mode recordings have a videoclip thumbnail. Deleted by
|
|
40063
40454
|
* EventStore.evictTracklessBefore on the DEVICE's one retention window
|
|
40064
40455
|
* (follow-recordings horizon, or trackRetentionDays, default 7), which
|
|
@@ -40093,7 +40484,7 @@ var AUDIO_EVENTS_COLLECTION = "pipeline-analytics:audio-events";
|
|
|
40093
40484
|
var COMMON_BASE_COLUMNS = [
|
|
40094
40485
|
{
|
|
40095
40486
|
name: "id",
|
|
40096
|
-
type: "
|
|
40487
|
+
type: "INTEGER",
|
|
40097
40488
|
primaryKey: true,
|
|
40098
40489
|
notNull: true
|
|
40099
40490
|
},
|
|
@@ -40126,7 +40517,31 @@ var MOTION_COLUMNS = [
|
|
|
40126
40517
|
{
|
|
40127
40518
|
name: "frameHeight",
|
|
40128
40519
|
type: "INTEGER"
|
|
40129
|
-
}
|
|
40520
|
+
},
|
|
40521
|
+
(
|
|
40522
|
+
/**
|
|
40523
|
+
* `null` while the episode this row represents is still open; the final
|
|
40524
|
+
* `lastOnAt - startedAt` once closed (D475). DECLARED, not merely stamped —
|
|
40525
|
+
* the sqlite backend silently drops an undeclared key on every write, which
|
|
40526
|
+
* is exactly how `frameId` was written into the void for years (see
|
|
40527
|
+
* `OBJECT_COLUMNS.frameId`). Absent on a row written before D475: that
|
|
40528
|
+
* means "closed the old way", never "open" — only an explicit `null` here
|
|
40529
|
+
* means open.
|
|
40530
|
+
*/
|
|
40531
|
+
{
|
|
40532
|
+
name: "durationMs",
|
|
40533
|
+
type: "INTEGER"
|
|
40534
|
+
}),
|
|
40535
|
+
(
|
|
40536
|
+
/**
|
|
40537
|
+
* Encoded rising-edge offsets for the episode — see `motion-edge-codec.ts`.
|
|
40538
|
+
* Written once, in the same UPDATE as `durationMs`, at close; a legacy or
|
|
40539
|
+
* still-open row has no value here, which the codec reads as `[]`.
|
|
40540
|
+
*/
|
|
40541
|
+
{
|
|
40542
|
+
name: "edges",
|
|
40543
|
+
type: "TEXT"
|
|
40544
|
+
})
|
|
40130
40545
|
];
|
|
40131
40546
|
var OBJECT_COLUMNS = [
|
|
40132
40547
|
...COMMON_BASE_COLUMNS,
|
|
@@ -40152,6 +40567,18 @@ var OBJECT_COLUMNS = [
|
|
|
40152
40567
|
name: "frameId",
|
|
40153
40568
|
type: "TEXT"
|
|
40154
40569
|
}),
|
|
40570
|
+
(
|
|
40571
|
+
/**
|
|
40572
|
+
* The producer's own idempotence key (D474) — see `ObjectEvent.idempotencyKey`.
|
|
40573
|
+
*
|
|
40574
|
+
* DECLARED, not merely stamped: the sqlite backend drops an undeclared key
|
|
40575
|
+
* without error, which is exactly how `frameId` was written into the void on
|
|
40576
|
+
* every row ever persisted. Nullable — only the package detector sets it.
|
|
40577
|
+
*/
|
|
40578
|
+
{
|
|
40579
|
+
name: "idempotencyKey",
|
|
40580
|
+
type: "TEXT"
|
|
40581
|
+
}),
|
|
40155
40582
|
{
|
|
40156
40583
|
name: "trackId",
|
|
40157
40584
|
type: "TEXT",
|
|
@@ -40285,6 +40712,42 @@ var COMMON_INDEXES = (prefix) => [{
|
|
|
40285
40712
|
name: `idx_${prefix}_device_ts`,
|
|
40286
40713
|
columns: ["deviceId", "timestamp"]
|
|
40287
40714
|
}];
|
|
40715
|
+
/**
|
|
40716
|
+
* The wire value `durationMs` carries for an OPEN episode (D475).
|
|
40717
|
+
*
|
|
40718
|
+
* The app-level contract is "`null` = open, a number = closed, absent = a
|
|
40719
|
+
* row from before this column existed" — but SQL has no way to tell "this
|
|
40720
|
+
* column was never given a value because the row predates it" from
|
|
40721
|
+
* "explicitly set to NULL": `declareCollection` adds the column via ALTER
|
|
40722
|
+
* TABLE, which backfills every existing row's `durationMs` with a real SQL
|
|
40723
|
+
* NULL, bit-for-bit the same NULL an open episode would write. Writing an
|
|
40724
|
+
* actual NULL for "open" would therefore make a brand-new open episode
|
|
40725
|
+
* indistinguishable from a five-year-old legacy row — exactly the failure
|
|
40726
|
+
* mode this constant exists to avoid. `insertMotion` writes this in place of
|
|
40727
|
+
* `null`; `queryMotion` translates it back to `null` on the way out, so
|
|
40728
|
+
* nothing outside this file ever sees the sentinel. `-1` is safe because a
|
|
40729
|
+
* real duration is never negative.
|
|
40730
|
+
*/
|
|
40731
|
+
var OPEN_EPISODE_SENTINEL = -1;
|
|
40732
|
+
/**
|
|
40733
|
+
* The key `insert` reports, narrowed to the ROWID these three collections use.
|
|
40734
|
+
*
|
|
40735
|
+
* `settingsStore.insert` answers `string | number` because the SAME method
|
|
40736
|
+
* serves TEXT-keyed collections, where it mints a UUID (D474). On the event
|
|
40737
|
+
* collections the primary key is an `INTEGER` rowid alias, so a string here is
|
|
40738
|
+
* not an event id at all — it means the table in front of us still has the old
|
|
40739
|
+
* TEXT key. Returning `null` says "no id", which is what every caller already
|
|
40740
|
+
* handles; the WARN says why, because a silent null would read as a failed
|
|
40741
|
+
* write (D474).
|
|
40742
|
+
*/
|
|
40743
|
+
function eventRowId(logger, operation, deviceId, key) {
|
|
40744
|
+
if (typeof key === "number") return key;
|
|
40745
|
+
logger.warn(`${operation}: event key is not an INTEGER rowid`, {
|
|
40746
|
+
tags: { deviceId },
|
|
40747
|
+
meta: { returnedId: key }
|
|
40748
|
+
});
|
|
40749
|
+
return null;
|
|
40750
|
+
}
|
|
40288
40751
|
var EventStore = class {
|
|
40289
40752
|
store;
|
|
40290
40753
|
logger;
|
|
@@ -40323,65 +40786,129 @@ var EventStore = class {
|
|
|
40323
40786
|
indexes: COMMON_INDEXES("audio")
|
|
40324
40787
|
});
|
|
40325
40788
|
}
|
|
40789
|
+
/**
|
|
40790
|
+
* Returns the ROWID SQLite assigned, or `null` when the write failed (D474).
|
|
40791
|
+
*
|
|
40792
|
+
* The caller needs it on the very next line — the notification centre reads
|
|
40793
|
+
* it and the marker-still path builds a media key from it — and with an
|
|
40794
|
+
* auto-assigned key the insert is the ONLY place it is knowable. A
|
|
40795
|
+
* placeholder would reach the event bus and the media keys.
|
|
40796
|
+
*/
|
|
40326
40797
|
async insertMotion(ev) {
|
|
40327
40798
|
this.onMotionSample?.(ev.deviceId, ev.regionCount, ev.timestamp);
|
|
40799
|
+
const record = { ...ev };
|
|
40800
|
+
if (ev.durationMs === null) record["durationMs"] = OPEN_EPISODE_SENTINEL;
|
|
40328
40801
|
try {
|
|
40329
|
-
const
|
|
40330
|
-
await this.store.insert.mutate({
|
|
40802
|
+
const inserted = await this.store.insert.mutate({
|
|
40331
40803
|
collection: MOTION_EVENTS_COLLECTION,
|
|
40332
|
-
record: {
|
|
40333
|
-
id,
|
|
40334
|
-
data: rest
|
|
40335
|
-
}
|
|
40804
|
+
record: { data: record }
|
|
40336
40805
|
});
|
|
40806
|
+
return eventRowId(this.logger, "insertMotion", ev.deviceId, inserted.id);
|
|
40337
40807
|
} catch (err) {
|
|
40338
40808
|
this.logger.warn("insertMotion failed", {
|
|
40339
40809
|
tags: { deviceId: ev.deviceId },
|
|
40340
40810
|
meta: {
|
|
40341
|
-
|
|
40811
|
+
timestamp: ev.timestamp,
|
|
40812
|
+
error: String(err)
|
|
40813
|
+
}
|
|
40814
|
+
});
|
|
40815
|
+
return null;
|
|
40816
|
+
}
|
|
40817
|
+
}
|
|
40818
|
+
/**
|
|
40819
|
+
* The one place a motion EPISODE row is closed (D475). Writes `durationMs`
|
|
40820
|
+
* and the encoded `edges` in a single UPDATE — a row never gets a second
|
|
40821
|
+
* write after this one. Returns whether it landed; the caller
|
|
40822
|
+
* (`MotionEpisodeTracker`) logs the failure with the episode's `deviceId`
|
|
40823
|
+
* tag, because a `durationMs: null` row that never gets a reason is exactly
|
|
40824
|
+
* the silent-forever-open shape D475 exists to prevent.
|
|
40825
|
+
*/
|
|
40826
|
+
async updateMotion(eventId, deviceId, patch) {
|
|
40827
|
+
try {
|
|
40828
|
+
await this.store.update.mutate({
|
|
40829
|
+
collection: MOTION_EVENTS_COLLECTION,
|
|
40830
|
+
id: String(eventId),
|
|
40831
|
+
data: {
|
|
40832
|
+
durationMs: patch.durationMs,
|
|
40833
|
+
edges: encodeMotionEdges(patch.edges)
|
|
40834
|
+
}
|
|
40835
|
+
});
|
|
40836
|
+
return true;
|
|
40837
|
+
} catch (err) {
|
|
40838
|
+
this.logger.warn("updateMotion (episode close) failed", {
|
|
40839
|
+
tags: { deviceId },
|
|
40840
|
+
meta: {
|
|
40841
|
+
eventId,
|
|
40342
40842
|
error: String(err)
|
|
40343
40843
|
}
|
|
40344
40844
|
});
|
|
40845
|
+
return false;
|
|
40345
40846
|
}
|
|
40346
40847
|
}
|
|
40848
|
+
/**
|
|
40849
|
+
* The motion-activity LTS tap, exposed for a caller that extends an
|
|
40850
|
+
* already-open episode without inserting a new row (D475) — `insertMotion`
|
|
40851
|
+
* fires it on OPEN; this fires it on every further tick, so a long
|
|
40852
|
+
* continuous episode still samples activity at the same density the
|
|
40853
|
+
* pre-D475 heartbeat gave the aggregator, rather than once per episode.
|
|
40854
|
+
*/
|
|
40855
|
+
noteMotionSample(deviceId, regionCount, at) {
|
|
40856
|
+
this.onMotionSample?.(deviceId, regionCount, at);
|
|
40857
|
+
}
|
|
40858
|
+
/**
|
|
40859
|
+
* Returns the ROWID SQLite assigned, or `null` when the write failed (D474).
|
|
40860
|
+
*
|
|
40861
|
+
* The caller needs it on the very next line — the notification centre reads
|
|
40862
|
+
* it and the marker-still path builds a media key from it — and with an
|
|
40863
|
+
* auto-assigned key the insert is the ONLY place it is knowable. A
|
|
40864
|
+
* placeholder would reach the event bus and the media keys.
|
|
40865
|
+
*/
|
|
40347
40866
|
async insertObject(ev) {
|
|
40348
40867
|
try {
|
|
40349
|
-
const
|
|
40350
|
-
|
|
40868
|
+
const rounded = "bbox" in ev ? {
|
|
40869
|
+
...ev,
|
|
40870
|
+
bbox: roundBbox(ev.bbox)
|
|
40871
|
+
} : ev;
|
|
40872
|
+
const inserted = await this.store.insert.mutate({
|
|
40351
40873
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
40352
|
-
record: {
|
|
40353
|
-
id,
|
|
40354
|
-
data: rest
|
|
40355
|
-
}
|
|
40874
|
+
record: { data: rounded }
|
|
40356
40875
|
});
|
|
40876
|
+
return eventRowId(this.logger, "insertObject", ev.deviceId, inserted.id);
|
|
40357
40877
|
} catch (err) {
|
|
40358
40878
|
this.logger.warn("insertObject failed", {
|
|
40359
40879
|
tags: { deviceId: ev.deviceId },
|
|
40360
40880
|
meta: {
|
|
40361
|
-
|
|
40881
|
+
timestamp: ev.timestamp,
|
|
40362
40882
|
error: String(err)
|
|
40363
40883
|
}
|
|
40364
40884
|
});
|
|
40885
|
+
return null;
|
|
40365
40886
|
}
|
|
40366
40887
|
}
|
|
40888
|
+
/**
|
|
40889
|
+
* Returns the ROWID SQLite assigned, or `null` when the write failed (D474).
|
|
40890
|
+
*
|
|
40891
|
+
* The caller needs it on the very next line — the notification centre reads
|
|
40892
|
+
* it and the marker-still path builds a media key from it — and with an
|
|
40893
|
+
* auto-assigned key the insert is the ONLY place it is knowable. A
|
|
40894
|
+
* placeholder would reach the event bus and the media keys.
|
|
40895
|
+
*/
|
|
40367
40896
|
async insertAudio(ev) {
|
|
40368
40897
|
try {
|
|
40369
|
-
const
|
|
40370
|
-
await this.store.insert.mutate({
|
|
40898
|
+
const inserted = await this.store.insert.mutate({
|
|
40371
40899
|
collection: AUDIO_EVENTS_COLLECTION,
|
|
40372
|
-
record: {
|
|
40373
|
-
id,
|
|
40374
|
-
data: rest
|
|
40375
|
-
}
|
|
40900
|
+
record: { data: ev }
|
|
40376
40901
|
});
|
|
40902
|
+
return eventRowId(this.logger, "insertAudio", ev.deviceId, inserted.id);
|
|
40377
40903
|
} catch (err) {
|
|
40378
40904
|
this.logger.warn("insertAudio failed", {
|
|
40379
40905
|
tags: { deviceId: ev.deviceId },
|
|
40380
40906
|
meta: {
|
|
40381
|
-
|
|
40907
|
+
timestamp: ev.timestamp,
|
|
40382
40908
|
error: String(err)
|
|
40383
40909
|
}
|
|
40384
40910
|
});
|
|
40911
|
+
return null;
|
|
40385
40912
|
}
|
|
40386
40913
|
}
|
|
40387
40914
|
/**
|
|
@@ -40495,12 +41022,15 @@ var EventStore = class {
|
|
|
40495
41022
|
filter: this.buildFilter(q),
|
|
40496
41023
|
...this.slimColumns(q.projection, SLIM_MOTION_COLUMNS)
|
|
40497
41024
|
});
|
|
40498
|
-
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls$1(r.data)));
|
|
41025
|
+
if (q.projection === "slim") return rows.map((r) => slimMotion(Number(r.id), stripNulls$1(r.data)));
|
|
40499
41026
|
return rows.map((r) => {
|
|
41027
|
+
const { edges: rawEdges, durationMs: rawDurationMs, ...data } = stripNulls$1(r.data);
|
|
40500
41028
|
return {
|
|
40501
|
-
id: r.id,
|
|
41029
|
+
id: Number(r.id),
|
|
40502
41030
|
kind: "motion",
|
|
40503
|
-
...
|
|
41031
|
+
...data,
|
|
41032
|
+
...rawDurationMs !== void 0 ? { durationMs: rawDurationMs === OPEN_EPISODE_SENTINEL ? null : rawDurationMs } : {},
|
|
41033
|
+
...rawEdges !== void 0 ? { edges: decodeMotionEdges(rawEdges) } : {}
|
|
40504
41034
|
};
|
|
40505
41035
|
});
|
|
40506
41036
|
}
|
|
@@ -40547,7 +41077,7 @@ var EventStore = class {
|
|
|
40547
41077
|
});
|
|
40548
41078
|
if (rows.length === 0) break;
|
|
40549
41079
|
for (const r of rows) out.push({
|
|
40550
|
-
id: r.id,
|
|
41080
|
+
id: Number(r.id),
|
|
40551
41081
|
kind: "object",
|
|
40552
41082
|
...stripNulls$1(r.data)
|
|
40553
41083
|
});
|
|
@@ -40566,10 +41096,10 @@ var EventStore = class {
|
|
|
40566
41096
|
filter,
|
|
40567
41097
|
...this.slimColumns(q.projection, SLIM_OBJECT_COLUMNS)
|
|
40568
41098
|
});
|
|
40569
|
-
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls$1(r.data)));
|
|
41099
|
+
if (q.projection === "slim") return rows.map((r) => slimObject(Number(r.id), stripNulls$1(r.data)));
|
|
40570
41100
|
return rows.map((r) => {
|
|
40571
41101
|
return {
|
|
40572
|
-
id: r.id,
|
|
41102
|
+
id: Number(r.id),
|
|
40573
41103
|
kind: "object",
|
|
40574
41104
|
...stripNulls$1(r.data)
|
|
40575
41105
|
};
|
|
@@ -40594,7 +41124,7 @@ var EventStore = class {
|
|
|
40594
41124
|
}
|
|
40595
41125
|
})).map((r) => {
|
|
40596
41126
|
return {
|
|
40597
|
-
id: r.id,
|
|
41127
|
+
id: Number(r.id),
|
|
40598
41128
|
kind: "object",
|
|
40599
41129
|
...stripNulls$1(r.data)
|
|
40600
41130
|
};
|
|
@@ -40605,10 +41135,10 @@ var EventStore = class {
|
|
|
40605
41135
|
collection: AUDIO_EVENTS_COLLECTION,
|
|
40606
41136
|
filter: this.buildFilter(q)
|
|
40607
41137
|
});
|
|
40608
|
-
if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls$1(r.data)));
|
|
41138
|
+
if (q.projection === "slim") return rows.map((r) => slimAudio(Number(r.id), stripNulls$1(r.data)));
|
|
40609
41139
|
return rows.map((r) => {
|
|
40610
41140
|
return {
|
|
40611
|
-
id: r.id,
|
|
41141
|
+
id: Number(r.id),
|
|
40612
41142
|
kind: "audio",
|
|
40613
41143
|
...stripNulls$1(r.data)
|
|
40614
41144
|
};
|
|
@@ -40619,9 +41149,28 @@ var EventStore = class {
|
|
|
40619
41149
|
* resolver's track fallback: a new-style object event owns no crop, so its
|
|
40620
41150
|
* `mediaUrl` degrades to the track's cadence media, addressed by this id.
|
|
40621
41151
|
* Returns `null` when the id is not a persisted object event (e.g. a KeyEvent
|
|
40622
|
-
* whose id is already a track id
|
|
40623
|
-
|
|
40624
|
-
|
|
41152
|
+
* whose id is already a track id) — best-effort.
|
|
41153
|
+
*
|
|
41154
|
+
* **D476: only an OBJECT event has a `trackId` at all** — motion and audio
|
|
41155
|
+
* rows carry no such column, so this probes `OBJECT_EVENTS_COLLECTION`
|
|
41156
|
+
* only, never the other two. That was already true structurally, but before
|
|
41157
|
+
* D474 an id that MISSED here was safely "not an object event" because
|
|
41158
|
+
* every id was a UUID, unique across all three tables. D474 made event ids
|
|
41159
|
+
* per-table rowid aliases, so a motion id can now coincide with an
|
|
41160
|
+
* UNRELATED object row of the same number — a probe that HITS is no longer
|
|
41161
|
+
* proof the caller's id names that object row.
|
|
41162
|
+
*
|
|
41163
|
+
* `ownerType`, when the caller already knows it (a `motion:`/`audio:`/
|
|
41164
|
+
* `object:` key was parsed rather than a bare legacy id), short-circuits to
|
|
41165
|
+
* `null` without querying for anything but `'object'` — the caller's one
|
|
41166
|
+
* remaining lever against the collision, since this method has nothing left
|
|
41167
|
+
* to guess once the kind is in the key. Its only caller today
|
|
41168
|
+
* (`readEventThumbnail` in `index.ts`) still reaches this with a bare,
|
|
41169
|
+
* kind-less id from a URL and cannot supply it — see the D476 ADR for why
|
|
41170
|
+
* that residual ambiguity is accepted rather than fixed here.
|
|
41171
|
+
*/
|
|
41172
|
+
async getTrackIdForEvent(eventId, ownerType) {
|
|
41173
|
+
if (ownerType !== void 0 && ownerType !== "object") return null;
|
|
40625
41174
|
try {
|
|
40626
41175
|
const row = await this.store.get.query({
|
|
40627
41176
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
@@ -40974,9 +41523,11 @@ var EventStore = class {
|
|
|
40974
41523
|
});
|
|
40975
41524
|
const motion = await batch(MOTION_EVENTS_COLLECTION);
|
|
40976
41525
|
const audio = await batch(AUDIO_EVENTS_COLLECTION);
|
|
40977
|
-
|
|
40978
|
-
|
|
40979
|
-
|
|
41526
|
+
if ((motion.length > 0 || audio.length > 0) && this.media !== void 0) try {
|
|
41527
|
+
await this.media.deleteForEvents({
|
|
41528
|
+
motion,
|
|
41529
|
+
audio
|
|
41530
|
+
});
|
|
40980
41531
|
} catch (err) {
|
|
40981
41532
|
this.logger.warn("EventStore.evictTracklessBefore: event media delete failed", { meta: { error: String(err) } });
|
|
40982
41533
|
}
|
|
@@ -41042,7 +41593,7 @@ var EventStore = class {
|
|
|
41042
41593
|
* {@link existingEventIds} answers "is this event alive", which is the
|
|
41043
41594
|
* orphan audit's question. The media reclaim asks a narrower one: *is this
|
|
41044
41595
|
* event-owned snapshot a MOTION-VISIT still* — because the selection shape
|
|
41045
|
-
* (`ownerKind: '
|
|
41596
|
+
* (`ownerKind: 'motion', kind: 'snapshot'`) is not itself a guarantee.
|
|
41046
41597
|
* Nothing structurally prevents another writer landing an event-owned
|
|
41047
41598
|
* snapshot on an object or audio row, and the reclaim must not be the place
|
|
41048
41599
|
* that discovers it. A per-page indexed `whereIn` on one table is cheap
|
|
@@ -41109,7 +41660,12 @@ var EventStore = class {
|
|
|
41109
41660
|
...motionIds,
|
|
41110
41661
|
...objectIds,
|
|
41111
41662
|
...audioIds
|
|
41112
|
-
]
|
|
41663
|
+
],
|
|
41664
|
+
byKind: {
|
|
41665
|
+
motion: motionIds,
|
|
41666
|
+
object: objectIds,
|
|
41667
|
+
audio: audioIds
|
|
41668
|
+
}
|
|
41113
41669
|
};
|
|
41114
41670
|
}
|
|
41115
41671
|
/**
|
|
@@ -41174,7 +41730,7 @@ var EventStore = class {
|
|
|
41174
41730
|
}
|
|
41175
41731
|
let mediaRows = 0;
|
|
41176
41732
|
if (eventIds.length > 0 && this.media !== void 0) try {
|
|
41177
|
-
mediaRows = await this.media.deleteForEvents(eventIds);
|
|
41733
|
+
mediaRows = await this.media.deleteForEvents({ object: eventIds });
|
|
41178
41734
|
} catch (err) {
|
|
41179
41735
|
this.logger.warn("EventStore.deleteByTracks: event media delete failed", { meta: { error: String(err) } });
|
|
41180
41736
|
}
|
|
@@ -41504,8 +42060,8 @@ function rowMatchesZone(data, zone) {
|
|
|
41504
42060
|
const fw = data["frameWidth"];
|
|
41505
42061
|
const fh = data["frameHeight"];
|
|
41506
42062
|
if (typeof fw !== "number" || typeof fh !== "number") return true;
|
|
41507
|
-
const positions = data["positions"];
|
|
41508
|
-
if (
|
|
42063
|
+
const positions = decodePositions(data["positions"]);
|
|
42064
|
+
if (positions.length === 0) return true;
|
|
41509
42065
|
const positionRows = [];
|
|
41510
42066
|
for (const p of positions) {
|
|
41511
42067
|
if (p === null || typeof p !== "object") continue;
|
|
@@ -43080,7 +43636,13 @@ var TrackStore = class {
|
|
|
43080
43636
|
data
|
|
43081
43637
|
});
|
|
43082
43638
|
matched.sort((a, b) => Number(b.data["firstSeen"] ?? 0) - Number(a.data["firstSeen"] ?? 0));
|
|
43083
|
-
return matched.slice(0, limit).map((r) =>
|
|
43639
|
+
return matched.slice(0, limit).map((r) => {
|
|
43640
|
+
const track = this.rowToTrack(r.id, r.data, params.projection);
|
|
43641
|
+
return {
|
|
43642
|
+
...track,
|
|
43643
|
+
positions: geometryOnlyPositions(track.positions)
|
|
43644
|
+
};
|
|
43645
|
+
});
|
|
43084
43646
|
}
|
|
43085
43647
|
/**
|
|
43086
43648
|
* Batched multi-device recent-tracks page (`listRecentTracks`): the
|
|
@@ -43335,7 +43897,7 @@ var TrackStore = class {
|
|
|
43335
43897
|
...t.producingDeviceName !== void 0 ? { producingDeviceName: t.producingDeviceName } : {},
|
|
43336
43898
|
firstSeen: t.firstSeen,
|
|
43337
43899
|
lastSeen: t.lastSeen,
|
|
43338
|
-
positions:
|
|
43900
|
+
positions: encodePositions(t.positions),
|
|
43339
43901
|
observations: t.positions.length,
|
|
43340
43902
|
snapshots: [...t.snapshots],
|
|
43341
43903
|
zonesVisited: [...t.zonesVisited],
|
|
@@ -43364,7 +43926,7 @@ var TrackStore = class {
|
|
|
43364
43926
|
...t.source !== void 0 ? { source: t.source } : {},
|
|
43365
43927
|
firstSeen: t.firstSeen,
|
|
43366
43928
|
lastSeen: t.lastSeen,
|
|
43367
|
-
positions:
|
|
43929
|
+
positions: encodePositions(t.positions),
|
|
43368
43930
|
observations: t.positions.length,
|
|
43369
43931
|
snapshots: [...t.snapshots],
|
|
43370
43932
|
zonesVisited: [...t.zonesVisited],
|
|
@@ -43404,7 +43966,7 @@ var TrackStore = class {
|
|
|
43404
43966
|
*/
|
|
43405
43967
|
rowToTrack(id, data, projection) {
|
|
43406
43968
|
const slim = projection === "slim";
|
|
43407
|
-
const storedPositions = slim ? [] : data["positions"]
|
|
43969
|
+
const storedPositions = slim ? [] : decodePositions(data["positions"]);
|
|
43408
43970
|
/**
|
|
43409
43971
|
* The stored count first, the blob only as a fallback — never `0` for a
|
|
43410
43972
|
* column that was not read.
|
|
@@ -44371,7 +44933,7 @@ function createDiscardJob(deps) {
|
|
|
44371
44933
|
collection: TRACKS_COLLECTION,
|
|
44372
44934
|
key: owner.ownerId
|
|
44373
44935
|
});
|
|
44374
|
-
} else if (owner.ownerKind === "
|
|
44936
|
+
} else if (owner.ownerKind === "object") await deps.store.delete.mutate({
|
|
44375
44937
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
44376
44938
|
key: owner.ownerId
|
|
44377
44939
|
});
|
|
@@ -45001,21 +45563,29 @@ function allEventIds(events) {
|
|
|
45001
45563
|
/**
|
|
45002
45564
|
* Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
|
|
45003
45565
|
* `resolveDefaultEventMedia`: the event's OWN media when it has any, else its
|
|
45004
|
-
* owning TRACK's.
|
|
45005
|
-
*
|
|
45006
|
-
*
|
|
45566
|
+
* owning TRACK's. Up to four batched index reads — one PER EVENT KIND that has
|
|
45567
|
+
* any ids (D476: `motion` / `object` / `audio` are separate owner kinds now,
|
|
45568
|
+
* so a single combined read can no longer ask "does any of these ids own
|
|
45569
|
+
* media" — a motion id and an object id can be the same number post-D474 and
|
|
45570
|
+
* are answered by different rows) — plus the track fallback, only over the
|
|
45571
|
+
* object events the first pass did not already answer for (motion and audio
|
|
45572
|
+
* are trackless roots, so they never need it).
|
|
45007
45573
|
*
|
|
45008
45574
|
* THROWS on a failed read. The caller fails closed on the CLAIM and open on the
|
|
45009
45575
|
* LIST; swallowing here would collapse those two opposite directions into one.
|
|
45010
45576
|
*/
|
|
45011
45577
|
async function resolveVouchedEvents(deviceId, events, fetchMediaOwners) {
|
|
45012
|
-
const
|
|
45013
|
-
|
|
45014
|
-
|
|
45015
|
-
|
|
45016
|
-
|
|
45017
|
-
|
|
45018
|
-
|
|
45578
|
+
const ownMedia = /* @__PURE__ */ new Set();
|
|
45579
|
+
for (const ownerType of EVENT_OWNER_TYPES) {
|
|
45580
|
+
const ownerIds = events[ownerType].map((e) => e.id);
|
|
45581
|
+
if (ownerIds.length === 0) continue;
|
|
45582
|
+
const found = await fetchMediaOwners({
|
|
45583
|
+
deviceId,
|
|
45584
|
+
ownerKind: ownerType,
|
|
45585
|
+
ownerIds
|
|
45586
|
+
});
|
|
45587
|
+
for (const id of found) ownMedia.add(id);
|
|
45588
|
+
}
|
|
45019
45589
|
const residualTracks = [];
|
|
45020
45590
|
const seenTracks = /* @__PURE__ */ new Set();
|
|
45021
45591
|
for (const event of events.object) {
|
|
@@ -45130,6 +45700,15 @@ function reportUnvouchedClips(deps, deviceId, clips) {
|
|
|
45130
45700
|
});
|
|
45131
45701
|
}
|
|
45132
45702
|
//#endregion
|
|
45703
|
+
//#region src/pipeline-analytics/videoclips-shared.ts
|
|
45704
|
+
/** Stringify the numeric event ids of one bucket. Lossless. */
|
|
45705
|
+
function toEventLike(events) {
|
|
45706
|
+
return events.map((e) => ({
|
|
45707
|
+
...e,
|
|
45708
|
+
id: String(e.id)
|
|
45709
|
+
}));
|
|
45710
|
+
}
|
|
45711
|
+
//#endregion
|
|
45133
45712
|
//#region src/pipeline-analytics/viewer-settings-actions.ts
|
|
45134
45713
|
/**
|
|
45135
45714
|
* Viewer settings snapshots — hub-shared named blobs of the viewer's stores.
|
|
@@ -46184,7 +46763,13 @@ var AnalyticsQueryFacade = class {
|
|
|
46184
46763
|
const store = this.deps.mediaStore();
|
|
46185
46764
|
if (store === null) return [];
|
|
46186
46765
|
const prefix = this.deps.eventMediaPathPrefix();
|
|
46187
|
-
|
|
46766
|
+
const kinds = input.kind === void 0 ? void 0 : [input.kind];
|
|
46767
|
+
let rows = [];
|
|
46768
|
+
for (const ownerType of EVENT_OWNER_TYPES) {
|
|
46769
|
+
rows = await store.listByOwner(ownerType, input.eventId, input.deviceId, kinds);
|
|
46770
|
+
if (rows.length > 0) break;
|
|
46771
|
+
}
|
|
46772
|
+
return rows.map((row) => toWireMediaFile(row, prefix));
|
|
46188
46773
|
}
|
|
46189
46774
|
/**
|
|
46190
46775
|
* A track's media WITH the bytes, restricted to `kinds` when given.
|
|
@@ -46226,7 +46811,12 @@ var AnalyticsQueryFacade = class {
|
|
|
46226
46811
|
const store = this.deps.mediaStore();
|
|
46227
46812
|
if (store === null) return [];
|
|
46228
46813
|
const prefix = this.deps.eventMediaPathPrefix();
|
|
46229
|
-
|
|
46814
|
+
let rows = [];
|
|
46815
|
+
for (const ownerType of EVENT_OWNER_TYPES) {
|
|
46816
|
+
rows = await store.listInfoByOwner(ownerType, input.eventId, input.deviceId);
|
|
46817
|
+
if (rows.length > 0) break;
|
|
46818
|
+
}
|
|
46819
|
+
return rows.map((row) => toWireMediaFileInfo(row, prefix));
|
|
46230
46820
|
}
|
|
46231
46821
|
/**
|
|
46232
46822
|
* A track's media WITHOUT the bytes.
|
|
@@ -46260,9 +46850,9 @@ var AnalyticsQueryFacade = class {
|
|
|
46260
46850
|
object: 0,
|
|
46261
46851
|
audio: 0
|
|
46262
46852
|
};
|
|
46263
|
-
const { counts, ids } = await eventStore.pruneBefore(input);
|
|
46853
|
+
const { counts, ids, byKind } = await eventStore.pruneBefore(input);
|
|
46264
46854
|
const mediaStore = this.deps.mediaStore();
|
|
46265
|
-
if (ids.length > 0 && mediaStore) await mediaStore.deleteForEvents(
|
|
46855
|
+
if (ids.length > 0 && mediaStore) await mediaStore.deleteForEvents(byKind);
|
|
46266
46856
|
const { motion, object, audio } = counts;
|
|
46267
46857
|
if (motion + object + audio > 0) this.deps.logger().info("analytics event eviction (floor)", {
|
|
46268
46858
|
tags: { deviceId: input.deviceId },
|
|
@@ -46384,7 +46974,11 @@ var AnalyticsQueryFacade = class {
|
|
|
46384
46974
|
...evicted.audio
|
|
46385
46975
|
];
|
|
46386
46976
|
const mediaStore = this.deps.mediaStore();
|
|
46387
|
-
if (ids.length > 0 && mediaStore) await mediaStore.deleteForEvents(
|
|
46977
|
+
if (ids.length > 0 && mediaStore) await mediaStore.deleteForEvents({
|
|
46978
|
+
motion: evicted.motion,
|
|
46979
|
+
object: objectIds,
|
|
46980
|
+
audio: evicted.audio
|
|
46981
|
+
});
|
|
46388
46982
|
const counts = {
|
|
46389
46983
|
motion: evicted.motion.length,
|
|
46390
46984
|
object: objectIds.length,
|
|
@@ -49223,9 +49817,13 @@ function resolveCropForcedMedia(files, pickClean, detailFiles = []) {
|
|
|
49223
49817
|
* Event + track media (the body/scene) and face/plate media (the detail
|
|
49224
49818
|
* crops). Face rows live under `face-${trackId}`; a track can close with
|
|
49225
49819
|
* `hasEmbeddedFace` and ZERO track files (0e343f86).
|
|
49820
|
+
*
|
|
49821
|
+
* `eventFiles` is the event's OWN media, already fetched by the caller (the
|
|
49822
|
+
* same rows the default path reuses via {@link collectDefaultEventMedia}) —
|
|
49823
|
+
* this function no longer lists by owner kind `'event'` itself (D476; see
|
|
49824
|
+
* {@link CropForcedOwnerKind}).
|
|
49226
49825
|
*/
|
|
49227
|
-
async function collectCropForcedMedia(source, id) {
|
|
49228
|
-
const eventFiles = await source.listByOwner("event", id);
|
|
49826
|
+
async function collectCropForcedMedia(source, id, eventFiles) {
|
|
49229
49827
|
const trackFiles = await source.listByOwner("track", id);
|
|
49230
49828
|
const ownerTrackId = trackFiles.length === 0 ? await source.getTrackIdForEvent(id) : null;
|
|
49231
49829
|
const owningTrackFiles = ownerTrackId === null ? [] : await source.listByOwner("track", ownerTrackId);
|
|
@@ -56179,11 +56777,23 @@ var PACKAGE_IMPORTANCE = 1;
|
|
|
56179
56777
|
* stay `getObjectEvents`-output-valid. */
|
|
56180
56778
|
var DELIVERED_STATE = "idle";
|
|
56181
56779
|
var PICKED_UP_STATE = "left";
|
|
56182
|
-
/**
|
|
56183
|
-
|
|
56780
|
+
/**
|
|
56781
|
+
* Deterministic IDEMPOTENCE keys, one per (stationary entry, phase).
|
|
56782
|
+
*
|
|
56783
|
+
* These were the event ids until D474 made an event id the SQLite rowid, which
|
|
56784
|
+
* the database assigns and a producer cannot choose. They kept their exact
|
|
56785
|
+
* spelling and moved to `ObjectEvent.idempotencyKey`: the gates below are the
|
|
56786
|
+
* only reason a restart does not re-deliver every parcel, and the strings are
|
|
56787
|
+
* on 2.1 million existing rows' primary key, so a row written before the
|
|
56788
|
+
* rebuild is still recognisable by the same text.
|
|
56789
|
+
*
|
|
56790
|
+
* They are NOT event ids any more. `eventId` on the emitted payload and in
|
|
56791
|
+
* every log line is the row's real id.
|
|
56792
|
+
*/
|
|
56793
|
+
function deliveredIdempotencyKey(entryId) {
|
|
56184
56794
|
return `pa-pkg-${entryId}-delivered`;
|
|
56185
56795
|
}
|
|
56186
|
-
function
|
|
56796
|
+
function pickedUpIdempotencyKey(entryId) {
|
|
56187
56797
|
return `pa-pkg-${entryId}-pickedup`;
|
|
56188
56798
|
}
|
|
56189
56799
|
var PackageDropDetector = class {
|
|
@@ -56321,26 +56931,28 @@ var PackageDropDetector = class {
|
|
|
56321
56931
|
});
|
|
56322
56932
|
return;
|
|
56323
56933
|
}
|
|
56324
|
-
const
|
|
56325
|
-
|
|
56934
|
+
const idempotencyKey = deliveredIdempotencyKey(entry.id);
|
|
56935
|
+
const alreadyDelivered = (await this.deps.events.queryObject({
|
|
56326
56936
|
deviceId: entry.deviceId,
|
|
56327
|
-
classFilter:
|
|
56328
|
-
})).
|
|
56937
|
+
classFilter: PACKAGE_EVENT_CLASS
|
|
56938
|
+
})).find((e) => e.idempotencyKey === idempotencyKey || String(e.id) === idempotencyKey);
|
|
56939
|
+
if (alreadyDelivered !== void 0) {
|
|
56329
56940
|
this.deps.logger.info("package gate 5/5 idempotence — delivery already recorded", {
|
|
56330
56941
|
tags,
|
|
56331
56942
|
meta: {
|
|
56332
56943
|
entryId: entry.id,
|
|
56333
|
-
|
|
56944
|
+
idempotencyKey,
|
|
56945
|
+
eventId: alreadyDelivered.id
|
|
56334
56946
|
}
|
|
56335
56947
|
});
|
|
56336
56948
|
return;
|
|
56337
56949
|
}
|
|
56338
|
-
const
|
|
56339
|
-
id: eventId,
|
|
56950
|
+
const draft = {
|
|
56340
56951
|
kind: "object",
|
|
56341
56952
|
deviceId: entry.deviceId,
|
|
56342
56953
|
timestamp,
|
|
56343
56954
|
source: "pipeline",
|
|
56955
|
+
idempotencyKey,
|
|
56344
56956
|
trackId: entry.sourceTrackId ?? entry.id,
|
|
56345
56957
|
className: PACKAGE_EVENT_CLASS,
|
|
56346
56958
|
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
@@ -56358,12 +56970,26 @@ var PackageDropDetector = class {
|
|
|
56358
56970
|
...entry.keyFrameMediaKey !== void 0 ? { mediaKey: entry.keyFrameMediaKey } : {},
|
|
56359
56971
|
importance: PACKAGE_IMPORTANCE
|
|
56360
56972
|
};
|
|
56361
|
-
await this.deps.events.insertObject(
|
|
56973
|
+
const rowId = await this.deps.events.insertObject(draft);
|
|
56974
|
+
if (rowId === null) {
|
|
56975
|
+
this.deps.logger.warn("package delivery NOT emitted — the event row did not persist", {
|
|
56976
|
+
tags,
|
|
56977
|
+
meta: {
|
|
56978
|
+
entryId: entry.id,
|
|
56979
|
+
idempotencyKey
|
|
56980
|
+
}
|
|
56981
|
+
});
|
|
56982
|
+
return;
|
|
56983
|
+
}
|
|
56984
|
+
const ev = {
|
|
56985
|
+
...draft,
|
|
56986
|
+
id: rowId
|
|
56987
|
+
};
|
|
56362
56988
|
this.deps.onPersisted?.(ev, "delivered");
|
|
56363
56989
|
this.deps.emit.delivered({
|
|
56364
56990
|
deviceId: entry.deviceId,
|
|
56365
56991
|
entryId: entry.id,
|
|
56366
|
-
eventId,
|
|
56992
|
+
eventId: String(rowId),
|
|
56367
56993
|
className: entry.className,
|
|
56368
56994
|
zoneIds: hitZones,
|
|
56369
56995
|
...entry.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: entry.keyFrameMediaKey } : {},
|
|
@@ -56379,7 +57005,8 @@ var PackageDropDetector = class {
|
|
|
56379
57005
|
tags: { deviceId: entry.deviceId },
|
|
56380
57006
|
meta: {
|
|
56381
57007
|
entryId: entry.id,
|
|
56382
|
-
eventId,
|
|
57008
|
+
eventId: rowId,
|
|
57009
|
+
idempotencyKey,
|
|
56383
57010
|
zoneIds: hitZones,
|
|
56384
57011
|
dwellMs,
|
|
56385
57012
|
areaFrac: Number(areaFrac.toFixed(5)),
|
|
@@ -56397,13 +57024,14 @@ var PackageDropDetector = class {
|
|
|
56397
57024
|
});
|
|
56398
57025
|
return;
|
|
56399
57026
|
}
|
|
56400
|
-
const
|
|
56401
|
-
const
|
|
57027
|
+
const deliveredKey = deliveredIdempotencyKey(entry.id);
|
|
57028
|
+
const pickedUpKey = pickedUpIdempotencyKey(entry.id);
|
|
56402
57029
|
const rows = await this.deps.events.queryObject({
|
|
56403
57030
|
deviceId: entry.deviceId,
|
|
56404
57031
|
classFilter: PACKAGE_EVENT_CLASS
|
|
56405
57032
|
});
|
|
56406
|
-
const
|
|
57033
|
+
const matchesKey = (e, key) => e.idempotencyKey === key || String(e.id) === key;
|
|
57034
|
+
const delivered = rows.find((e) => matchesKey(e, deliveredKey));
|
|
56407
57035
|
if (delivered === void 0) {
|
|
56408
57036
|
this.deps.logger.debug("package pick-up skipped — no delivery row for this entry", {
|
|
56409
57037
|
tags: { deviceId: entry.deviceId },
|
|
@@ -56414,12 +57042,14 @@ var PackageDropDetector = class {
|
|
|
56414
57042
|
});
|
|
56415
57043
|
return;
|
|
56416
57044
|
}
|
|
56417
|
-
|
|
57045
|
+
const alreadyPickedUp = rows.find((e) => matchesKey(e, pickedUpKey));
|
|
57046
|
+
if (alreadyPickedUp !== void 0) {
|
|
56418
57047
|
this.deps.logger.info("package pick-up skipped — already recorded", {
|
|
56419
57048
|
tags: { deviceId: entry.deviceId },
|
|
56420
57049
|
meta: {
|
|
56421
57050
|
entryId: entry.id,
|
|
56422
|
-
eventId:
|
|
57051
|
+
eventId: alreadyPickedUp.id,
|
|
57052
|
+
idempotencyKey: pickedUpKey
|
|
56423
57053
|
}
|
|
56424
57054
|
});
|
|
56425
57055
|
return;
|
|
@@ -56431,17 +57061,17 @@ var PackageDropDetector = class {
|
|
|
56431
57061
|
tags: { deviceId: entry.deviceId },
|
|
56432
57062
|
meta: {
|
|
56433
57063
|
entryId: entry.id,
|
|
56434
|
-
|
|
57064
|
+
idempotencyKey: pickedUpKey,
|
|
56435
57065
|
packageTrackId,
|
|
56436
57066
|
pickupAt: entry.lastConfirmedAt
|
|
56437
57067
|
}
|
|
56438
57068
|
});
|
|
56439
|
-
const
|
|
56440
|
-
id: pickedUpId,
|
|
57069
|
+
const draft = {
|
|
56441
57070
|
kind: "object",
|
|
56442
57071
|
deviceId: entry.deviceId,
|
|
56443
57072
|
timestamp,
|
|
56444
57073
|
source: "pipeline",
|
|
57074
|
+
idempotencyKey: pickedUpKey,
|
|
56445
57075
|
trackId: mediaTrackId,
|
|
56446
57076
|
className: PACKAGE_EVENT_CLASS,
|
|
56447
57077
|
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
@@ -56459,12 +57089,26 @@ var PackageDropDetector = class {
|
|
|
56459
57089
|
...collector?.keyFrameMediaKey !== void 0 ? { mediaKey: collector.keyFrameMediaKey } : {},
|
|
56460
57090
|
importance: PACKAGE_IMPORTANCE
|
|
56461
57091
|
};
|
|
56462
|
-
await this.deps.events.insertObject(
|
|
57092
|
+
const rowId = await this.deps.events.insertObject(draft);
|
|
57093
|
+
if (rowId === null) {
|
|
57094
|
+
this.deps.logger.warn("package pick-up NOT emitted — the event row did not persist", {
|
|
57095
|
+
tags: { deviceId: entry.deviceId },
|
|
57096
|
+
meta: {
|
|
57097
|
+
entryId: entry.id,
|
|
57098
|
+
idempotencyKey: pickedUpKey
|
|
57099
|
+
}
|
|
57100
|
+
});
|
|
57101
|
+
return;
|
|
57102
|
+
}
|
|
57103
|
+
const ev = {
|
|
57104
|
+
...draft,
|
|
57105
|
+
id: rowId
|
|
57106
|
+
};
|
|
56463
57107
|
this.deps.onPersisted?.(ev, "picked-up");
|
|
56464
57108
|
this.deps.emit.pickedUp({
|
|
56465
57109
|
deviceId: entry.deviceId,
|
|
56466
57110
|
entryId: entry.id,
|
|
56467
|
-
deliveredEventId:
|
|
57111
|
+
deliveredEventId: String(delivered.id),
|
|
56468
57112
|
className: entry.className,
|
|
56469
57113
|
...collector !== void 0 ? { collectorTrackId: collector.trackId } : {},
|
|
56470
57114
|
timestamp
|
|
@@ -56473,8 +57117,9 @@ var PackageDropDetector = class {
|
|
|
56473
57117
|
tags: { deviceId: entry.deviceId },
|
|
56474
57118
|
meta: {
|
|
56475
57119
|
entryId: entry.id,
|
|
56476
|
-
eventId:
|
|
56477
|
-
|
|
57120
|
+
eventId: rowId,
|
|
57121
|
+
idempotencyKey: pickedUpKey,
|
|
57122
|
+
deliveredEventId: delivered.id,
|
|
56478
57123
|
mediaTrackId,
|
|
56479
57124
|
...collector !== void 0 ? {
|
|
56480
57125
|
collectorTrackId: collector.trackId,
|
|
@@ -58909,7 +59554,7 @@ async function loadRebuildSource(deps, trackId) {
|
|
|
58909
59554
|
const cropInfo = media.find((m) => m.kind === "thumbnail");
|
|
58910
59555
|
const peak = await deps.events.peakForTrack(trackId);
|
|
58911
59556
|
const events = await deps.events.queryObjectByTrackIds([trackId]);
|
|
58912
|
-
const best = events.find((e) => e.id === peak.bestEventId) ?? events[0];
|
|
59557
|
+
const best = events.find((e) => String(e.id) === peak.bestEventId) ?? events[0];
|
|
58913
59558
|
if (best === void 0) return null;
|
|
58914
59559
|
const common = {
|
|
58915
59560
|
trackId,
|
|
@@ -62802,7 +63447,7 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
62802
63447
|
classification: {
|
|
62803
63448
|
kind: "owner-indirection",
|
|
62804
63449
|
derivation: "explicit",
|
|
62805
|
-
why: "(ownerKind, ownerId) means a different entity per kind — a trackId for 'track', a PREFIXED trackId for 'face'/'plate', an eventId for 'event', and an enrolled gallery row for 'identity'/'vehicle' that retention must never touch. A RETIRED ownerKind (the deleted group entity) resolves to an owner that can never exist again and is always an orphan. The mapping is `resolveMediaOwner` in orphan-audit.ts."
|
|
63450
|
+
why: "(ownerKind, ownerId) means a different entity per kind — a trackId for 'track', a PREFIXED trackId for 'face'/'plate', an eventId for 'motion'/'object'/'audio' (D476 split 'event' into the three tables it used to cover), and an enrolled gallery row for 'identity'/'vehicle' that retention must never touch. A RETIRED ownerKind (the deleted group entity) resolves to an owner that can never exist again and is always an orphan. The mapping is `resolveMediaOwner` in orphan-audit.ts."
|
|
62806
63451
|
}
|
|
62807
63452
|
},
|
|
62808
63453
|
{
|
|
@@ -63016,7 +63661,9 @@ function resolveMediaOwner(row) {
|
|
|
63016
63661
|
kind: "track",
|
|
63017
63662
|
trackId: ownerId
|
|
63018
63663
|
};
|
|
63019
|
-
case "
|
|
63664
|
+
case "motion":
|
|
63665
|
+
case "object":
|
|
63666
|
+
case "audio": return {
|
|
63020
63667
|
kind: "event",
|
|
63021
63668
|
eventId: ownerId
|
|
63022
63669
|
};
|
|
@@ -64285,8 +64932,21 @@ var FULL_FRAME_BBOX = {
|
|
|
64285
64932
|
w: 1,
|
|
64286
64933
|
h: 1
|
|
64287
64934
|
};
|
|
64935
|
+
/** The same classification the RUNNER applies to decide "full frame" from the
|
|
64936
|
+
* bbox (`getNativeCrop`), restated here so the two cannot drift. */
|
|
64937
|
+
function isFullFrameBbox(bbox) {
|
|
64938
|
+
return bbox.x <= .001 && bbox.y <= .001 && bbox.w >= .999 && bbox.h >= .999;
|
|
64939
|
+
}
|
|
64288
64940
|
/** Throttle for the native-crop HIT/FALLBACK metric window line. */
|
|
64289
64941
|
var NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
|
|
64942
|
+
/**
|
|
64943
|
+
* Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (the
|
|
64944
|
+
* `keyFrame`/group-shot rung, ~1–1.5 MB on a 4K camera) rather than a boxed
|
|
64945
|
+
* timeline tile or a pinned ≤640 detection frame (tens of kB). The two belong
|
|
64946
|
+
* in different counters or the window line says "full frames" and means two
|
|
64947
|
+
* payload classes three orders of magnitude apart. (D470)
|
|
64948
|
+
*/
|
|
64949
|
+
var FULL_FRAME_TILE_WIDTH_CEILING = 1920;
|
|
64290
64950
|
function createNativeFrameTransport(deps) {
|
|
64291
64951
|
const { api, logger } = deps;
|
|
64292
64952
|
const pipelineRunnerApi = api.pipelineRunner;
|
|
@@ -64360,16 +65020,35 @@ function createNativeFrameTransport(deps) {
|
|
|
64360
65020
|
tier: reply.tier === "ram-fullframe" ? "ram-fullframe" : "native"
|
|
64361
65021
|
};
|
|
64362
65022
|
};
|
|
64363
|
-
const getRemoteFrame = async (handle) => requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame);
|
|
64364
|
-
const fetchNativeFullFrameTiered = async (handle, maxWidth) => requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame);
|
|
65023
|
+
const getRemoteFrame = async (handle) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame));
|
|
65024
|
+
const fetchNativeFullFrameTiered = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame));
|
|
64365
65025
|
const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
|
|
64366
65026
|
const cropMetricLogger = logger.child("NativeCrop");
|
|
64367
65027
|
let nativeHits = 0;
|
|
64368
65028
|
let nativeFallbacks = 0;
|
|
64369
65029
|
let lastCropMetricAt = 0;
|
|
65030
|
+
let roiFetches = 0;
|
|
65031
|
+
let fullFrames = 0;
|
|
65032
|
+
let nativeFullFrames = 0;
|
|
64370
65033
|
const bumpCropMetric = (hit) => {
|
|
64371
65034
|
if (hit) nativeHits += 1;
|
|
64372
65035
|
else nativeFallbacks += 1;
|
|
65036
|
+
maybeLogCropWindow();
|
|
65037
|
+
};
|
|
65038
|
+
/**
|
|
65039
|
+
* Publish the window once `request` has SETTLED — never before, or the line
|
|
65040
|
+
* carries a window whose own outcome has not been counted yet and throttles
|
|
65041
|
+
* away the one that has. `finally`, so the deliberate throw of
|
|
65042
|
+
* `fetchNativeFullFrameTiered` still reaches `persistTrackKeyFrame`.
|
|
65043
|
+
*/
|
|
65044
|
+
const closeWindowAfter = async (request) => {
|
|
65045
|
+
try {
|
|
65046
|
+
return await request;
|
|
65047
|
+
} finally {
|
|
65048
|
+
maybeLogCropWindow();
|
|
65049
|
+
}
|
|
65050
|
+
};
|
|
65051
|
+
const maybeLogCropWindow = () => {
|
|
64373
65052
|
const now = Date.now();
|
|
64374
65053
|
if (now - lastCropMetricAt < NATIVE_CROP_METRIC_INTERVAL_MS) return;
|
|
64375
65054
|
lastCropMetricAt = now;
|
|
@@ -64379,11 +65058,20 @@ function createNativeFrameTransport(deps) {
|
|
|
64379
65058
|
detectionFrameFallbacks: nativeFallbacks,
|
|
64380
65059
|
wireFetches: dedupe.fetched,
|
|
64381
65060
|
coalesced: dedupe.coalesced,
|
|
64382
|
-
reusedInWindow: dedupe.reused
|
|
65061
|
+
reusedInWindow: dedupe.reused,
|
|
65062
|
+
roiCalls: roiFetches,
|
|
65063
|
+
tileFullFrameCalls: fullFrames,
|
|
65064
|
+
nativeFullFrameCalls: nativeFullFrames
|
|
64383
65065
|
} });
|
|
65066
|
+
roiFetches = 0;
|
|
65067
|
+
fullFrames = 0;
|
|
65068
|
+
nativeFullFrames = 0;
|
|
64384
65069
|
};
|
|
64385
65070
|
const requestNativeCrop = async (frameHandle, bbox, maxWidth, normalize) => {
|
|
64386
65071
|
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
65072
|
+
if (isFullFrameBbox(bbox)) if (maxWidth !== void 0 && maxWidth > FULL_FRAME_TILE_WIDTH_CEILING) nativeFullFrames += 1;
|
|
65073
|
+
else fullFrames += 1;
|
|
65074
|
+
else roiFetches += 1;
|
|
64387
65075
|
const native = await pipelineRunnerApi.getNativeCrop.query({
|
|
64388
65076
|
handle: frameHandle,
|
|
64389
65077
|
bbox,
|
|
@@ -66432,7 +67120,7 @@ var EventMediaDispatcher = class {
|
|
|
66432
67120
|
if (!childCropData) continue;
|
|
66433
67121
|
await this.deps.mediaStore.put({
|
|
66434
67122
|
deviceId,
|
|
66435
|
-
ownerKind: "
|
|
67123
|
+
ownerKind: "object",
|
|
66436
67124
|
ownerId: ev.eventId,
|
|
66437
67125
|
kind: child.kind,
|
|
66438
67126
|
timestamp: ev.timestamp,
|
|
@@ -66596,7 +67284,7 @@ var MotionEventSnapshotCapturer = class {
|
|
|
66596
67284
|
try {
|
|
66597
67285
|
await this.deps.media.put({
|
|
66598
67286
|
deviceId: input.deviceId,
|
|
66599
|
-
ownerKind: "
|
|
67287
|
+
ownerKind: "motion",
|
|
66600
67288
|
ownerId: input.eventId,
|
|
66601
67289
|
kind: "snapshot",
|
|
66602
67290
|
timestamp: input.timestamp,
|
|
@@ -69569,48 +70257,111 @@ function compareSummaryScores(a, b) {
|
|
|
69569
70257
|
if (a.visible !== b.visible) return a.visible - b.visible;
|
|
69570
70258
|
return a.quality - b.quality;
|
|
69571
70259
|
}
|
|
69572
|
-
/** A candidate replaces the best only when it is STRICTLY better — a tie keeps
|
|
69573
|
-
* the pixels already materialised (they cost a native round trip). */
|
|
69574
|
-
function isBetterSummaryScore(candidate, best) {
|
|
69575
|
-
if (candidate.visible === 0) return false;
|
|
69576
|
-
return best === null || compareSummaryScores(candidate, best) > 0;
|
|
69577
|
-
}
|
|
69578
|
-
//#endregion
|
|
69579
|
-
//#region src/pipeline-analytics/summary/group-shot.ts
|
|
69580
|
-
/**
|
|
69581
|
-
* The running argmax of one open summary's group shot.
|
|
69582
|
-
*
|
|
69583
|
-
* RAM only, on purpose: it decides WHEN pixels are worth a native round trip
|
|
69584
|
-
* (only when the maximum improves — D9/D18), it is not the record of the
|
|
69585
|
-
* shot. The record is the media row the caller writes under the summary's
|
|
69586
|
-
* id; after a restart an open summary simply starts its argmax again and
|
|
69587
|
-
* replaces the row the next time it beats what it can remember, which is
|
|
69588
|
-
* nothing. That is a weaker shot for one session, never a wrong one.
|
|
69589
|
-
*/
|
|
69590
70260
|
var GroupShotTracker = class {
|
|
69591
|
-
|
|
70261
|
+
states = /* @__PURE__ */ new Map();
|
|
70262
|
+
offered = 0;
|
|
70263
|
+
materialised = 0;
|
|
70264
|
+
byGap = 0;
|
|
70265
|
+
byMargin = 0;
|
|
70266
|
+
byWorse = 0;
|
|
69592
70267
|
/**
|
|
69593
|
-
* Offer a frame. Returns `true` when it
|
|
69594
|
-
*
|
|
69595
|
-
*
|
|
70268
|
+
* Offer a frame. Returns `true` when it is worth materialising NOW — the
|
|
70269
|
+
* caller's cue to fetch the pixels for THIS frame, while its handle is
|
|
70270
|
+
* still leased.
|
|
70271
|
+
*
|
|
70272
|
+
* The clock is `frame.timestamp`; the caller injects no second one.
|
|
69596
70273
|
*/
|
|
69597
70274
|
offer(summaryId, score, frame) {
|
|
69598
|
-
if (
|
|
69599
|
-
this.
|
|
70275
|
+
if (score.visible === 0) return false;
|
|
70276
|
+
this.offered += 1;
|
|
70277
|
+
const state = this.states.get(summaryId);
|
|
70278
|
+
if (state === void 0) {
|
|
70279
|
+
this.states.set(summaryId, {
|
|
70280
|
+
delivered: null,
|
|
70281
|
+
lastAttemptAt: frame.timestamp,
|
|
70282
|
+
unlanded: 1
|
|
70283
|
+
});
|
|
70284
|
+
this.materialised += 1;
|
|
70285
|
+
return true;
|
|
70286
|
+
}
|
|
70287
|
+
if (state.lastAttemptAt !== void 0 && frame.timestamp - state.lastAttemptAt < 2e3) {
|
|
70288
|
+
this.byGap += 1;
|
|
70289
|
+
return false;
|
|
70290
|
+
}
|
|
70291
|
+
if (!this.isWorthPixels(state, score)) return false;
|
|
70292
|
+
state.lastAttemptAt = frame.timestamp;
|
|
70293
|
+
state.unlanded += 1;
|
|
70294
|
+
this.materialised += 1;
|
|
70295
|
+
return true;
|
|
70296
|
+
}
|
|
70297
|
+
/**
|
|
70298
|
+
* Nothing has landed yet → keep trying (D58) until the budget is spent.
|
|
70299
|
+
* Something has landed → beat it, by the margin on the quality tiebreak.
|
|
70300
|
+
*/
|
|
70301
|
+
isWorthPixels(state, score) {
|
|
70302
|
+
const delivered = state.delivered;
|
|
70303
|
+
if (delivered === null) {
|
|
70304
|
+
if (state.unlanded >= 4) {
|
|
70305
|
+
this.byMargin += 1;
|
|
70306
|
+
return false;
|
|
70307
|
+
}
|
|
70308
|
+
return true;
|
|
70309
|
+
}
|
|
70310
|
+
if (compareSummaryScores(score, delivered.score) <= 0) {
|
|
70311
|
+
this.byWorse += 1;
|
|
70312
|
+
return false;
|
|
70313
|
+
}
|
|
70314
|
+
if (score.visible === delivered.score.visible && score.quality - delivered.score.quality < .05) {
|
|
70315
|
+
this.byMargin += 1;
|
|
70316
|
+
return false;
|
|
70317
|
+
}
|
|
70318
|
+
return true;
|
|
70319
|
+
}
|
|
70320
|
+
/**
|
|
70321
|
+
* A materialisation LANDED. Only this advances the baseline the next
|
|
70322
|
+
* candidate must beat — a fetch that missed leaves the summary exactly as
|
|
70323
|
+
* picture-less as it was, and the next frame may be worse and still worth
|
|
70324
|
+
* taking.
|
|
70325
|
+
*/
|
|
70326
|
+
noteLanded(summaryId, score, frame) {
|
|
70327
|
+
const state = this.states.get(summaryId);
|
|
70328
|
+
if (state === void 0) return;
|
|
70329
|
+
state.delivered = {
|
|
69600
70330
|
score,
|
|
69601
70331
|
frame
|
|
69602
|
-
}
|
|
69603
|
-
|
|
70332
|
+
};
|
|
70333
|
+
state.unlanded = 0;
|
|
69604
70334
|
}
|
|
69605
|
-
|
|
69606
|
-
|
|
70335
|
+
/** The shot on disk for this summary, as this process last saw it land. */
|
|
70336
|
+
deliveredOf(summaryId) {
|
|
70337
|
+
return this.states.get(summaryId)?.delivered ?? null;
|
|
69607
70338
|
}
|
|
69608
70339
|
/** A sealed or evicted summary takes no more frames; drop its state. */
|
|
69609
70340
|
forget(summaryId) {
|
|
69610
|
-
this.
|
|
70341
|
+
this.states.delete(summaryId);
|
|
70342
|
+
}
|
|
70343
|
+
/**
|
|
70344
|
+
* What the gate did since the last read. Shipped WITH the gate: without it
|
|
70345
|
+
* the only evidence for a fetch that did NOT happen is hub-main's byte
|
|
70346
|
+
* counter, which answers a different question a minute later.
|
|
70347
|
+
*/
|
|
70348
|
+
drainCounts() {
|
|
70349
|
+
const counts = {
|
|
70350
|
+
offered: this.offered,
|
|
70351
|
+
materialised: this.materialised,
|
|
70352
|
+
byGap: this.byGap,
|
|
70353
|
+
byMargin: this.byMargin,
|
|
70354
|
+
byWorse: this.byWorse
|
|
70355
|
+
};
|
|
70356
|
+
this.offered = 0;
|
|
70357
|
+
this.materialised = 0;
|
|
70358
|
+
this.byGap = 0;
|
|
70359
|
+
this.byMargin = 0;
|
|
70360
|
+
this.byWorse = 0;
|
|
70361
|
+
return counts;
|
|
69611
70362
|
}
|
|
69612
70363
|
get size() {
|
|
69613
|
-
return this.
|
|
70364
|
+
return this.states.size;
|
|
69614
70365
|
}
|
|
69615
70366
|
};
|
|
69616
70367
|
//#endregion
|
|
@@ -69907,7 +70658,7 @@ async function buildTrainingExportPlan(readers, input) {
|
|
|
69907
70658
|
scope: "plate"
|
|
69908
70659
|
},
|
|
69909
70660
|
...(eventsByTrack.get(track.trackId) ?? []).map((eventId) => ({
|
|
69910
|
-
ownerKind: "
|
|
70661
|
+
ownerKind: "object",
|
|
69911
70662
|
ownerId: eventId,
|
|
69912
70663
|
scope: `events/${eventId}`
|
|
69913
70664
|
}))
|
|
@@ -70749,6 +71500,8 @@ var RETRAIN_EXPORT_MAX = 2e4;
|
|
|
70749
71500
|
* surface to turn the refinement pipeline on/off for a camera.
|
|
70750
71501
|
*/
|
|
70751
71502
|
var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
71503
|
+
/** Throttle for the group-shot gate's window line (D470). */
|
|
71504
|
+
var GROUP_SHOT_METRIC_INTERVAL_MS = 3e4;
|
|
70752
71505
|
/**
|
|
70753
71506
|
* How often the addon asks "is a gallery sample outside the cluster face
|
|
70754
71507
|
* space?". A flip is rare and the check is two cheap reads — a minute keeps
|
|
@@ -70876,7 +71629,6 @@ var AUDIO_EPISODE_PERSIST_MS = 2e3;
|
|
|
70876
71629
|
* the persistence (D62).
|
|
70877
71630
|
*/
|
|
70878
71631
|
var AUDIO_EVENT_HEARTBEAT_MS = 5e3;
|
|
70879
|
-
var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
70880
71632
|
/**
|
|
70881
71633
|
* Stored media kinds that carry NO drawn bounding box, in fallback preference
|
|
70882
71634
|
* order. The reel forces `?kind=crop`; when a track has no crop the endpoint may
|
|
@@ -71005,8 +71757,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71005
71757
|
summaryStore = null;
|
|
71006
71758
|
/** The ONLY writer of a summary's lifecycle (D359). */
|
|
71007
71759
|
summarySealer = null;
|
|
71008
|
-
/** The
|
|
71760
|
+
/** The materialisation gate on every open summary's group shot (D359 phase 3, D470). RAM only. */
|
|
71009
71761
|
groupShots = new GroupShotTracker();
|
|
71762
|
+
/** Throttle for the group-shot gate's window line (D470). */
|
|
71763
|
+
lastGroupShotMetricAt = 0;
|
|
71010
71764
|
/**
|
|
71011
71765
|
* Tracks a gallery match has named, for the group-shot score's recognition
|
|
71012
71766
|
* term. "Recognised so far" rather than "at that instant": the match arrives
|
|
@@ -71443,7 +72197,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71443
72197
|
secondarySubjects = new SecondarySubjectQueue();
|
|
71444
72198
|
lastFrameDimsByDevice = /* @__PURE__ */ new Map();
|
|
71445
72199
|
lastAudioInsertByDevice = /* @__PURE__ */ new Map();
|
|
71446
|
-
|
|
72200
|
+
lastMotionTickByDevice = /* @__PURE__ */ new Map();
|
|
72201
|
+
motionEpisodes = null;
|
|
71447
72202
|
levelStateByDevice = /* @__PURE__ */ new Map();
|
|
71448
72203
|
/** Per-device corroboration window for classified audio (see
|
|
71449
72204
|
* `audio-confirm-window.ts`). Torn down wherever `levelStateByDevice` is —
|
|
@@ -72612,6 +73367,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
72612
73367
|
media: this.mediaStore
|
|
72613
73368
|
});
|
|
72614
73369
|
this.eventStore = eventStore;
|
|
73370
|
+
this.motionEpisodes = new MotionEpisodeTracker({
|
|
73371
|
+
closeAfterMs: MOTION_CLOSE_AFTER_MS,
|
|
73372
|
+
logger: logger.child("MotionEpisodeTracker"),
|
|
73373
|
+
insertRow: (input) => this.insertMotionEpisodeRow(input.deviceId, input.timestamp, input.regionData),
|
|
73374
|
+
closeRow: (input) => this.closeMotionEpisodeRow(input),
|
|
73375
|
+
onNewEdge: (input) => this.onMotionEpisodeEdge(input),
|
|
73376
|
+
onTick: (input) => this.onMotionEpisodeTick(input)
|
|
73377
|
+
});
|
|
72615
73378
|
this.sensorEventStore = new SensorEventStore({
|
|
72616
73379
|
store: api.settingsStore,
|
|
72617
73380
|
logger: logger.child("SensorEventStore")
|
|
@@ -73270,11 +74033,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73270
74033
|
* The mosaic is filed through the SAME `mediaStore` every event thumbnail
|
|
73271
74034
|
* goes through — the operator's directive, verbatim: same data structure,
|
|
73272
74035
|
* same storage location, no second retained store. It is written under
|
|
73273
|
-
* `ownerKind: '
|
|
73274
|
-
*
|
|
73275
|
-
*
|
|
73276
|
-
*
|
|
73277
|
-
*
|
|
74036
|
+
* `ownerKind: 'object'` with the window's own SYNTHETIC owner id (never a
|
|
74037
|
+
* real object-event id), so it lands beside the cameras' event media
|
|
74038
|
+
* (`<deviceId>/events/object/<ownerId>/…`) and is swept by the same
|
|
74039
|
+
* ownership-based orphan audit: `resolveMediaOwner` maps any of
|
|
74040
|
+
* `motion`/`object`/`audio` to the same generic `{kind:'event'}` OwnerRef,
|
|
74041
|
+
* `EventStore.existingEventIds` never finds the synthetic id in any of the
|
|
74042
|
+
* three tables, and the row is reclaimed as an orphan once past the grace
|
|
74043
|
+
* window — same effective lifecycle as before D476, when `event` covered
|
|
74044
|
+
* all three. `object` is a pragmatic choice among equals here, not a
|
|
74045
|
+
* structural fact about the mosaic (it is not itself an object-event row);
|
|
74046
|
+
* flagged in the D476 ADR as a spot a future dedicated owner kind may suit
|
|
74047
|
+
* better. It is deliberately NOT owned by a track: a track's media list is a
|
|
74048
|
+
* LIVE viewer surface, and a cross-camera mosaic appearing in one track's
|
|
74049
|
+
* tiles would be a picture that lies about what it is of.
|
|
73278
74050
|
*/
|
|
73279
74051
|
buildSummaryPorts(stores) {
|
|
73280
74052
|
return {
|
|
@@ -73315,7 +74087,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73315
74087
|
},
|
|
73316
74088
|
storeMosaic: async ({ deviceId, ownerId, timestamp, jpeg }) => stores.mediaStore.put({
|
|
73317
74089
|
deviceId,
|
|
73318
|
-
ownerKind: "
|
|
74090
|
+
ownerKind: "object",
|
|
73319
74091
|
ownerId,
|
|
73320
74092
|
kind: "fullFrame",
|
|
73321
74093
|
timestamp,
|
|
@@ -73555,7 +74327,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73555
74327
|
listEventIdsForTracks: async (trackIds) => {
|
|
73556
74328
|
if (!eventStore || trackIds.length === 0) return [];
|
|
73557
74329
|
return (await eventStore.queryObjectByTrackIds(trackIds)).flatMap((e) => e.trackId === void 0 ? [] : [{
|
|
73558
|
-
eventId: e.id,
|
|
74330
|
+
eventId: String(e.id),
|
|
73559
74331
|
trackId: e.trackId
|
|
73560
74332
|
}]);
|
|
73561
74333
|
},
|
|
@@ -74351,9 +75123,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
74351
75123
|
})
|
|
74352
75124
|
]);
|
|
74353
75125
|
return {
|
|
74354
|
-
motion,
|
|
74355
|
-
object,
|
|
74356
|
-
audio
|
|
75126
|
+
motion: toEventLike(motion),
|
|
75127
|
+
object: toEventLike(object),
|
|
75128
|
+
audio: toEventLike(audio)
|
|
74357
75129
|
};
|
|
74358
75130
|
},
|
|
74359
75131
|
fetchMediaOwners: async ({ deviceId, ownerKind, ownerIds }) => {
|
|
@@ -74508,6 +75280,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
74508
75280
|
this.alarmPanelConvergence = null;
|
|
74509
75281
|
for (const unsub of this.subscriptionUnsubs) unsub();
|
|
74510
75282
|
this.subscriptionUnsubs = [];
|
|
75283
|
+
await this.motionEpisodes?.closeAll("addon-shutdown");
|
|
74511
75284
|
await this.embeddingDispatcher?.stop();
|
|
74512
75285
|
this.embeddingDispatcher = null;
|
|
74513
75286
|
await this.notificationCenter?.stop();
|
|
@@ -75112,11 +75885,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75112
75885
|
byState
|
|
75113
75886
|
} });
|
|
75114
75887
|
}
|
|
75115
|
-
await
|
|
75888
|
+
const objectEvents = await this.persistObjectEvents(deviceId, result.objectEvents);
|
|
75889
|
+
const appearanceEvents = await this.persistObjectEvents(deviceId, result.appearanceEvents);
|
|
75116
75890
|
if (this.notificationCenter !== null) {
|
|
75117
75891
|
const overlaps = processor.getLastZoneOverlaps();
|
|
75118
75892
|
const rejections = processor.getLastZoneRejections();
|
|
75119
|
-
for (const e of
|
|
75893
|
+
for (const e of objectEvents) {
|
|
75120
75894
|
if (e.zones && e.zones.length > 0) {
|
|
75121
75895
|
const m = e.trackId ? overlaps.get(e.trackId) : void 0;
|
|
75122
75896
|
this.ctx.logger.info("zone membership stamped on event", {
|
|
@@ -75170,7 +75944,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75170
75944
|
const faceGloballyEnabled = this.faceRecognizer ? await this.resolveGlobalFaceEnabled() : false;
|
|
75171
75945
|
const mediaSettings = await this.resolveDeviceMediaSettings(deviceId);
|
|
75172
75946
|
if (this.eventMediaDispatcher && frameHandle) {
|
|
75173
|
-
const mediaEvents = [...
|
|
75947
|
+
const mediaEvents = [...objectEvents, ...appearanceEvents];
|
|
75174
75948
|
const childCropsByEvent = buildEventChildCrops(mediaEvents, frame.detections);
|
|
75175
75949
|
const uncroppable = mediaEvents.filter((e) => e.bbox === void 0 || e.bbox.w <= 0 || e.bbox.h <= 0);
|
|
75176
75950
|
if (uncroppable.length > 0) this.ctx.logger.debug("event media: no crop for events with no box in this frame (a departed track has nothing to cut)", {
|
|
@@ -75184,7 +75958,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75184
75958
|
const eventTargets = mediaEvents.filter((e) => e.bbox !== void 0 && e.bbox.w > 0 && e.bbox.h > 0).map((e) => {
|
|
75185
75959
|
const childCrops = childCropsByEvent.get(e.id);
|
|
75186
75960
|
return {
|
|
75187
|
-
eventId: e.id,
|
|
75961
|
+
eventId: String(e.id),
|
|
75188
75962
|
timestamp: e.timestamp,
|
|
75189
75963
|
bbox: e.bbox,
|
|
75190
75964
|
className: e.className,
|
|
@@ -75338,7 +76112,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75338
76112
|
...frameHandle !== void 0 ? { frameHandle } : {}
|
|
75339
76113
|
});
|
|
75340
76114
|
if (objectEmbeddingBests.length > 0) this.persistObjectEmbeddingBests(deviceId, result.timestamp, objectEmbeddingBests, frameHandle, result.frameWidth, result.frameHeight, mediaSettings.cropPadding);
|
|
75341
|
-
for (const e of
|
|
76115
|
+
for (const e of objectEvents) this.ctx.eventBus.emit({
|
|
75342
76116
|
id: `pa-${e.id}`,
|
|
75343
76117
|
timestamp: new Date(e.timestamp),
|
|
75344
76118
|
source: {
|
|
@@ -75436,6 +76210,45 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75436
76210
|
* eight settings kinds funnel through on a cache miss, so calling the raw
|
|
75437
76211
|
* reads directly here means eight kinds expiring in the same window each
|
|
75438
76212
|
* pay for the same two reads — see `store/settings-raw-mirror.ts`. */
|
|
76213
|
+
/**
|
|
76214
|
+
* Insert the frame's object-event DRAFTS and return them with the id the
|
|
76215
|
+
* database actually assigned (D474).
|
|
76216
|
+
*
|
|
76217
|
+
* The frame path cannot name an event: the id is a SQLite rowid, minted by
|
|
76218
|
+
* the write. So this is the ONE place a draft becomes an `ObjectEvent`, and
|
|
76219
|
+
* everything downstream — the media targets, the notification-centre hook,
|
|
76220
|
+
* the bus emit — consumes what comes back rather than what went in.
|
|
76221
|
+
*
|
|
76222
|
+
* Order is preserved. A draft whose row did not land is DROPPED and SAID:
|
|
76223
|
+
* it owns no media, can be named on no bus and can match no rule, and an
|
|
76224
|
+
* invented id would put `event:NaN` in a media key. A branch that accepts
|
|
76225
|
+
* work and produces nothing must log it.
|
|
76226
|
+
*/
|
|
76227
|
+
async persistObjectEvents(deviceId, drafts) {
|
|
76228
|
+
if (drafts.length === 0) return [];
|
|
76229
|
+
const store = this.eventStore;
|
|
76230
|
+
if (store === null) return [];
|
|
76231
|
+
const ids = await Promise.all(drafts.map((d) => store.insertObject(d)));
|
|
76232
|
+
const persisted = [];
|
|
76233
|
+
for (const [i, draft] of drafts.entries()) {
|
|
76234
|
+
const id = ids[i];
|
|
76235
|
+
if (id === void 0 || id === null) continue;
|
|
76236
|
+
persisted.push({
|
|
76237
|
+
...draft,
|
|
76238
|
+
id
|
|
76239
|
+
});
|
|
76240
|
+
}
|
|
76241
|
+
const lost = drafts.length - persisted.length;
|
|
76242
|
+
if (lost > 0) this.ctx.logger.warn("object events did not persist — no id, so no media, no bus, no rule", {
|
|
76243
|
+
tags: { deviceId },
|
|
76244
|
+
meta: {
|
|
76245
|
+
lost,
|
|
76246
|
+
of: drafts.length,
|
|
76247
|
+
trackIds: [...new Set(drafts.map((d) => d.trackId ?? "none"))]
|
|
76248
|
+
}
|
|
76249
|
+
});
|
|
76250
|
+
return persisted;
|
|
76251
|
+
}
|
|
75439
76252
|
async readDeviceSettings(deviceId, resolve) {
|
|
75440
76253
|
const globalRaw = this.ctxIfReady !== null ? await this.rawGlobalStoreMirror.resolve() : {};
|
|
75441
76254
|
const deviceRaw = await this.rawDeviceBlobMirror.resolve(deviceId);
|
|
@@ -76214,7 +77027,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76214
77027
|
recognized: this.recognizedTrackIds.has(t.trackId)
|
|
76215
77028
|
}))
|
|
76216
77029
|
});
|
|
76217
|
-
|
|
77030
|
+
const frameRef = { timestamp: result.timestamp };
|
|
77031
|
+
const worthPixels = this.groupShots.offer(open.id, score, frameRef);
|
|
77032
|
+
this.logGroupShotWindow(deviceId);
|
|
77033
|
+
if (!worthPixels) return;
|
|
76218
77034
|
const getNative = this.getNativeKeyFrameRgb;
|
|
76219
77035
|
const mediaStore = this.mediaStore;
|
|
76220
77036
|
if (!getNative || !mediaStore) return;
|
|
@@ -76236,17 +77052,48 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76236
77052
|
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
|
|
76237
77053
|
})
|
|
76238
77054
|
}).then((outcome) => {
|
|
76239
|
-
if (outcome?.kind === "landed")
|
|
76240
|
-
|
|
76241
|
-
|
|
76242
|
-
|
|
76243
|
-
|
|
76244
|
-
|
|
76245
|
-
|
|
76246
|
-
|
|
77055
|
+
if (outcome?.kind === "landed") {
|
|
77056
|
+
this.groupShots.noteLanded(summaryId, score, frameRef);
|
|
77057
|
+
this.ctx.logger.debug("summary group shot landed", {
|
|
77058
|
+
tags: { deviceId },
|
|
77059
|
+
meta: {
|
|
77060
|
+
summaryId,
|
|
77061
|
+
visible: score.visible,
|
|
77062
|
+
quality: Number(score.quality.toFixed(3))
|
|
77063
|
+
}
|
|
77064
|
+
});
|
|
77065
|
+
}
|
|
76247
77066
|
}).catch(() => {});
|
|
76248
77067
|
}
|
|
76249
77068
|
/**
|
|
77069
|
+
* The group-shot gate's window, throttled to one line per
|
|
77070
|
+
* {@link GROUP_SHOT_METRIC_INTERVAL_MS}. Process-global counts, tagged with
|
|
77071
|
+
* the device that happened to close the window — the counters are the
|
|
77072
|
+
* gate's, not the camera's, and the tag is there because every line about a
|
|
77073
|
+
* device carries one.
|
|
77074
|
+
*
|
|
77075
|
+
* `materialised` is the number of 4096-px native full-frame fetches this
|
|
77076
|
+
* path actually spent; `byGap` + `byMargin` + `byWorse` is what it did not.
|
|
77077
|
+
*/
|
|
77078
|
+
logGroupShotWindow(deviceId) {
|
|
77079
|
+
const now = Date.now();
|
|
77080
|
+
if (now - this.lastGroupShotMetricAt < GROUP_SHOT_METRIC_INTERVAL_MS) return;
|
|
77081
|
+
this.lastGroupShotMetricAt = now;
|
|
77082
|
+
const counts = this.groupShots.drainCounts();
|
|
77083
|
+
if (counts.offered === 0) return;
|
|
77084
|
+
this.ctx.logger.info("group shot window", {
|
|
77085
|
+
tags: { deviceId },
|
|
77086
|
+
meta: {
|
|
77087
|
+
offered: counts.offered,
|
|
77088
|
+
materialised: counts.materialised,
|
|
77089
|
+
suppressedByGap: counts.byGap,
|
|
77090
|
+
suppressedByMargin: counts.byMargin,
|
|
77091
|
+
suppressedByWorse: counts.byWorse,
|
|
77092
|
+
openSummaries: this.groupShots.size
|
|
77093
|
+
}
|
|
77094
|
+
});
|
|
77095
|
+
}
|
|
77096
|
+
/**
|
|
76250
77097
|
* Order the parcels a best-frame decision wants (D422). Skipped entirely at
|
|
76251
77098
|
* a node the registry already called offline (D58) — logged on the
|
|
76252
77099
|
* transition by `noteCapturesGated`, counted in the key-frame window.
|
|
@@ -76792,9 +77639,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76792
77639
|
* The two mirrors answer different halves and neither subsumes the other:
|
|
76793
77640
|
* - {@link lastTrackActivityMs} — a tracked object was alive. Present only
|
|
76794
77641
|
* while the camera is ATTACHED and producing inference frames.
|
|
76795
|
-
* - {@link
|
|
76796
|
-
* onboard). Covers a camera whose detector found nothing,
|
|
76797
|
-
* on `detectionMode: 'on-motion'` in the window before it
|
|
77642
|
+
* - {@link lastMotionTickByDevice} — motion was reported (analyzer or
|
|
77643
|
+
* onboard, ANY source). Covers a camera whose detector found nothing,
|
|
77644
|
+
* and a camera on `detectionMode: 'on-motion'` in the window before it
|
|
77645
|
+
* attaches. This is a plain per-device mirror, deliberately NOT the
|
|
77646
|
+
* per-source episode state in {@link motionEpisodes} — this question is
|
|
77647
|
+
* "is something visual happening on the camera", not "which episode".
|
|
76798
77648
|
*
|
|
76799
77649
|
* Staleness is safe in the direction that matters: a mirror that is late
|
|
76800
77650
|
* suppresses a marker, it never invents one.
|
|
@@ -76802,7 +77652,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76802
77652
|
isMotionActive(deviceId, atMs, quietMs) {
|
|
76803
77653
|
const trackAt = this.lastTrackActivityMs.get(deviceId);
|
|
76804
77654
|
if (trackAt !== void 0 && atMs - trackAt < quietMs) return true;
|
|
76805
|
-
const motion = this.
|
|
77655
|
+
const motion = this.lastMotionTickByDevice.get(deviceId);
|
|
76806
77656
|
return motion !== void 0 && motion.detected && atMs - motion.atMs < quietMs;
|
|
76807
77657
|
}
|
|
76808
77658
|
/** Per-device audio-marker config, folded from the cached audio settings. */
|
|
@@ -76867,8 +77717,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76867
77717
|
* therefore persisted nothing at all.
|
|
76868
77718
|
*/
|
|
76869
77719
|
async persistClassifiedAudio(input) {
|
|
76870
|
-
const
|
|
76871
|
-
id: randomUUID(),
|
|
77720
|
+
const draft = {
|
|
76872
77721
|
deviceId: input.deviceId,
|
|
76873
77722
|
timestamp: input.timestamp,
|
|
76874
77723
|
kind: "audio",
|
|
@@ -76884,7 +77733,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76884
77733
|
className: input.className,
|
|
76885
77734
|
atMs: input.timestamp
|
|
76886
77735
|
});
|
|
76887
|
-
await this.eventStore.insertAudio(
|
|
77736
|
+
const insertedId = await this.eventStore.insertAudio(draft);
|
|
77737
|
+
if (insertedId === null) return;
|
|
77738
|
+
const ev = {
|
|
77739
|
+
...draft,
|
|
77740
|
+
id: insertedId
|
|
77741
|
+
};
|
|
76888
77742
|
this.notificationCenter?.onAudioEventPersisted(ev);
|
|
76889
77743
|
this.trackStore?.addAudioLabelEpisode(input.deviceId, input.className, input.score, input.timestamp);
|
|
76890
77744
|
this.ctx.eventBus.emit({
|
|
@@ -77095,8 +77949,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77095
77949
|
});
|
|
77096
77950
|
this.levelStateByDevice.set(deviceId, res.nextState);
|
|
77097
77951
|
if (!res.emit) return;
|
|
77098
|
-
const
|
|
77099
|
-
id: randomUUID(),
|
|
77952
|
+
const draft = {
|
|
77100
77953
|
deviceId,
|
|
77101
77954
|
timestamp,
|
|
77102
77955
|
kind: "audio",
|
|
@@ -77107,7 +77960,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77107
77960
|
className: void 0,
|
|
77108
77961
|
atMs: timestamp
|
|
77109
77962
|
});
|
|
77110
|
-
await this.eventStore.insertAudio(
|
|
77963
|
+
const insertedId = await this.eventStore.insertAudio(draft);
|
|
77964
|
+
if (insertedId === null) return;
|
|
77965
|
+
const ev = {
|
|
77966
|
+
...draft,
|
|
77967
|
+
id: insertedId
|
|
77968
|
+
};
|
|
77111
77969
|
this.ctx.eventBus.emit({
|
|
77112
77970
|
id: `pa-${ev.id}`,
|
|
77113
77971
|
timestamp: new Date(ev.timestamp),
|
|
@@ -77126,10 +77984,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77126
77984
|
});
|
|
77127
77985
|
}
|
|
77128
77986
|
/**
|
|
77129
|
-
* Persist motion-analysis bus events
|
|
77130
|
-
*
|
|
77131
|
-
*
|
|
77132
|
-
*
|
|
77987
|
+
* Persist motion-analysis bus events into the EPISODE tracker (D475): a
|
|
77988
|
+
* rising edge with no open episode INSERTs a row; a further edge inside an
|
|
77989
|
+
* open episode extends it in place; the episode closes on its own timer,
|
|
77990
|
+
* `MOTION_CLOSE_AFTER_MS` after the last rising edge. See
|
|
77991
|
+
* `motion-episode.ts` / `motion-episode-tracker.ts` for the state machine.
|
|
77133
77992
|
*
|
|
77134
77993
|
* Gated on the same `pipeline-analytics` wrapper binding as the
|
|
77135
77994
|
* inference + audio handlers so toggling the wrapper off for a
|
|
@@ -77139,22 +77998,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77139
77998
|
if (this.shuttingDown) return;
|
|
77140
77999
|
if (!await this.bindingCache.isActive(deviceId)) return;
|
|
77141
78000
|
const detected = payload.detected === true;
|
|
77142
|
-
|
|
77143
|
-
|
|
77144
|
-
|
|
77145
|
-
|
|
77146
|
-
|
|
77147
|
-
|
|
77148
|
-
atMs: timestamp
|
|
77149
|
-
});
|
|
77150
|
-
return;
|
|
77151
|
-
}
|
|
77152
|
-
if (!transitionedOn && !heartbeatDue) return;
|
|
77153
|
-
const ev = {
|
|
77154
|
-
id: randomUUID(),
|
|
77155
|
-
deviceId,
|
|
77156
|
-
timestamp,
|
|
77157
|
-
kind: "motion",
|
|
78001
|
+
this.lastMotionTickByDevice.set(deviceId, {
|
|
78002
|
+
detected,
|
|
78003
|
+
atMs: timestamp
|
|
78004
|
+
});
|
|
78005
|
+
if (detected) this.eventStore?.noteMotionSample(deviceId, payload.regionCount, timestamp);
|
|
78006
|
+
await this.motionEpisodes?.observe(deviceId, "analyzer", detected, timestamp, {
|
|
77158
78007
|
regionCount: payload.regionCount,
|
|
77159
78008
|
regions: payload.regions.map((r) => ({
|
|
77160
78009
|
bbox: {
|
|
@@ -77168,69 +78017,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77168
78017
|
})),
|
|
77169
78018
|
frameWidth: payload.frameWidth,
|
|
77170
78019
|
frameHeight: payload.frameHeight
|
|
77171
|
-
};
|
|
77172
|
-
this.lastMotionInsertByDevice.set(deviceId, {
|
|
77173
|
-
detected,
|
|
77174
|
-
atMs: timestamp
|
|
77175
|
-
});
|
|
77176
|
-
if (transitionedOn) this.motionOnsets.noteRisingEdge(deviceId, timestamp);
|
|
77177
|
-
this.sceneEngine?.noteMotion(deviceId, timestamp);
|
|
77178
|
-
await this.eventStore.insertMotion(ev);
|
|
77179
|
-
this.ctx.eventBus.emit({
|
|
77180
|
-
id: `pa-${ev.id}`,
|
|
77181
|
-
timestamp: new Date(ev.timestamp),
|
|
77182
|
-
source: {
|
|
77183
|
-
type: "addon",
|
|
77184
|
-
id: "pipeline-analytics",
|
|
77185
|
-
addonId: "pipeline-analytics"
|
|
77186
|
-
},
|
|
77187
|
-
category: EventCategory.PipelineAnalyticsDetectionEvent,
|
|
77188
|
-
data: {
|
|
77189
|
-
deviceId,
|
|
77190
|
-
kind: "motion",
|
|
77191
|
-
eventId: ev.id,
|
|
77192
|
-
timestamp: ev.timestamp
|
|
77193
|
-
}
|
|
77194
78020
|
});
|
|
77195
|
-
if (transitionedOn) await this.captureMotionMarkerStill(deviceId, ev.id, ev.timestamp);
|
|
77196
78021
|
}
|
|
77197
78022
|
/**
|
|
77198
|
-
* Persist firmware-driven (onboard) motion events
|
|
77199
|
-
* {@link handleMotionAnalysis}
|
|
77200
|
-
*
|
|
77201
|
-
*
|
|
77202
|
-
* shape which lacks frame
|
|
77203
|
-
* (firmware reports a binary
|
|
77204
|
-
* coarse bbox via the optional
|
|
77205
|
-
*
|
|
77206
|
-
* The same `lastMotionInsertByDevice` map is shared with the analyzer
|
|
77207
|
-
* path so a camera that briefly switches `motionSources` between
|
|
77208
|
-
* `onboard` and `analyzer` gets consistent throttling — both paths
|
|
77209
|
-
* are mutually exclusive at the event-emit layer (the runner only
|
|
77210
|
-
* fires `MotionAnalysis` when its analyzer runs; onboard providers
|
|
77211
|
-
* never fire `MotionAnalysis`), so there's no double-count risk.
|
|
78023
|
+
* Persist firmware-driven (onboard / device-activity) motion events into
|
|
78024
|
+
* the SAME episode tracker as {@link handleMotionAnalysis}, keyed per
|
|
78025
|
+
* `(deviceId, source)` so the two paths can never share — or fight over —
|
|
78026
|
+
* one episode on a camera whose `motionSources` lists both (D475). Reads
|
|
78027
|
+
* from the {@link MotionOnMotionChangedPayload} shape which lacks frame
|
|
78028
|
+
* dimensions and pixel-level region details (firmware reports a binary
|
|
78029
|
+
* detected flag plus, on rare devices, a coarse bbox via the optional
|
|
78030
|
+
* `regions` field).
|
|
77212
78031
|
*/
|
|
77213
78032
|
async handleOnboardMotion(deviceId, payload, timestamp) {
|
|
77214
78033
|
if (this.shuttingDown) return;
|
|
77215
78034
|
if (!await this.bindingCache.isActive(deviceId)) return;
|
|
77216
78035
|
const detected = payload.detected === true;
|
|
77217
|
-
|
|
77218
|
-
|
|
77219
|
-
|
|
77220
|
-
|
|
77221
|
-
if (last?.detected === true) this.lastMotionInsertByDevice.set(deviceId, {
|
|
77222
|
-
detected: false,
|
|
77223
|
-
atMs: timestamp
|
|
77224
|
-
});
|
|
77225
|
-
return;
|
|
77226
|
-
}
|
|
77227
|
-
if (!transitionedOn && !heartbeatDue) return;
|
|
78036
|
+
this.lastMotionTickByDevice.set(deviceId, {
|
|
78037
|
+
detected,
|
|
78038
|
+
atMs: timestamp
|
|
78039
|
+
});
|
|
77228
78040
|
const regions = payload.regions ?? [];
|
|
77229
|
-
|
|
77230
|
-
|
|
77231
|
-
deviceId,
|
|
77232
|
-
timestamp,
|
|
77233
|
-
kind: "motion",
|
|
78041
|
+
if (detected) this.eventStore?.noteMotionSample(deviceId, regions.length, timestamp);
|
|
78042
|
+
await this.motionEpisodes?.observe(deviceId, payload.source, detected, timestamp, {
|
|
77234
78043
|
regionCount: regions.length,
|
|
77235
78044
|
regions: regions.map((r) => ({
|
|
77236
78045
|
bbox: {
|
|
@@ -77244,17 +78053,56 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77244
78053
|
})),
|
|
77245
78054
|
frameWidth: 0,
|
|
77246
78055
|
frameHeight: 0
|
|
78056
|
+
});
|
|
78057
|
+
}
|
|
78058
|
+
/**
|
|
78059
|
+
* `MotionEpisodeTracker.insertRow` — builds and inserts the row that opens
|
|
78060
|
+
* a motion episode. `regionData` carries the per-kind fields the two motion
|
|
78061
|
+
* handlers differ on (regions/frameWidth/frameHeight); everything else
|
|
78062
|
+
* (id, durationMs, edges) is the episode's own concern, not the caller's.
|
|
78063
|
+
*/
|
|
78064
|
+
async insertMotionEpisodeRow(deviceId, timestamp, regionData) {
|
|
78065
|
+
const draft = {
|
|
78066
|
+
deviceId,
|
|
78067
|
+
timestamp,
|
|
78068
|
+
kind: "motion",
|
|
78069
|
+
durationMs: null,
|
|
78070
|
+
...regionData
|
|
77247
78071
|
};
|
|
77248
|
-
this.
|
|
77249
|
-
|
|
77250
|
-
|
|
78072
|
+
return this.eventStore.insertMotion(draft);
|
|
78073
|
+
}
|
|
78074
|
+
/**
|
|
78075
|
+
* `MotionEpisodeTracker.closeRow` — the UPDATE that closes an episode.
|
|
78076
|
+
*/
|
|
78077
|
+
async closeMotionEpisodeRow(input) {
|
|
78078
|
+
return this.eventStore.updateMotion(input.eventId, input.deviceId, {
|
|
78079
|
+
durationMs: input.durationMs,
|
|
78080
|
+
edges: input.edges
|
|
78081
|
+
});
|
|
78082
|
+
}
|
|
78083
|
+
/**
|
|
78084
|
+
* `MotionEpisodeTracker.onNewEdge` — the side effects the pre-D475 code
|
|
78085
|
+
* fired on every `transitionedOn`: the birth-latency onset mark (D409, only
|
|
78086
|
+
* the FIRST edge of a burst moves it — a further edge inside the same
|
|
78087
|
+
* episode is a fresh onset in its own right, so it moves too) and the
|
|
78088
|
+
* motion-marker still (self-throttled by `motion-marker-policy.ts`;
|
|
78089
|
+
* `String` because a media key is a URL component while the id itself
|
|
78090
|
+
* stays a number, D474).
|
|
78091
|
+
*/
|
|
78092
|
+
onMotionEpisodeEdge(input) {
|
|
78093
|
+
this.motionOnsets.noteRisingEdge(input.deviceId, input.timestamp);
|
|
78094
|
+
this.captureMotionMarkerStill(input.deviceId, String(input.eventId), input.timestamp).catch((err) => {
|
|
78095
|
+
this.ctx.logger.debug("motion marker still capture failed", {
|
|
78096
|
+
tags: { deviceId: input.deviceId },
|
|
78097
|
+
meta: {
|
|
78098
|
+
eventId: input.eventId,
|
|
78099
|
+
error: errMsg$1(err)
|
|
78100
|
+
}
|
|
78101
|
+
});
|
|
77251
78102
|
});
|
|
77252
|
-
if (transitionedOn) this.motionOnsets.noteRisingEdge(deviceId, timestamp);
|
|
77253
|
-
this.sceneEngine?.noteMotion(deviceId, timestamp);
|
|
77254
|
-
await this.eventStore.insertMotion(ev);
|
|
77255
78103
|
this.ctx.eventBus.emit({
|
|
77256
|
-
id: `pa-${
|
|
77257
|
-
timestamp: new Date(
|
|
78104
|
+
id: `pa-${input.eventId}`,
|
|
78105
|
+
timestamp: new Date(input.timestamp),
|
|
77258
78106
|
source: {
|
|
77259
78107
|
type: "addon",
|
|
77260
78108
|
id: "pipeline-analytics",
|
|
@@ -77262,13 +78110,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
77262
78110
|
},
|
|
77263
78111
|
category: EventCategory.PipelineAnalyticsDetectionEvent,
|
|
77264
78112
|
data: {
|
|
77265
|
-
deviceId,
|
|
78113
|
+
deviceId: input.deviceId,
|
|
77266
78114
|
kind: "motion",
|
|
77267
|
-
eventId:
|
|
77268
|
-
timestamp:
|
|
78115
|
+
eventId: input.eventId,
|
|
78116
|
+
timestamp: input.timestamp
|
|
77269
78117
|
}
|
|
77270
78118
|
});
|
|
77271
|
-
|
|
78119
|
+
}
|
|
78120
|
+
/**
|
|
78121
|
+
* `MotionEpisodeTracker.onTick` — fires on every `detected:true`
|
|
78122
|
+
* observation that reaches an open or newly-opened episode, transition or
|
|
78123
|
+
* continuation. Arms the scene quiet-edge trigger: a scene is checked
|
|
78124
|
+
* `quietSeconds` AFTER the last motion, never during it, so this wants
|
|
78125
|
+
* "motion is still happening" — the SAME question `onNewEdge` does not
|
|
78126
|
+
* answer, since a long continuous burst is mostly continuation ticks.
|
|
78127
|
+
*/
|
|
78128
|
+
onMotionEpisodeTick(input) {
|
|
78129
|
+
this.sceneEngine?.noteMotion(input.deviceId, input.timestamp);
|
|
77272
78130
|
}
|
|
77273
78131
|
/**
|
|
77274
78132
|
* The gate in front of the motion still. Both motion handlers reach it, so
|
|
@@ -79836,7 +80694,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
79836
80694
|
deps: {
|
|
79837
80695
|
page: ({ deviceId, scope, afterMs, limit }) => mediaStore.pageByKindForDevice({
|
|
79838
80696
|
deviceId,
|
|
79839
|
-
ownerKind: scope === "motion-still" ? "
|
|
80697
|
+
ownerKind: scope === "motion-still" ? "motion" : "track",
|
|
79840
80698
|
kind: "snapshot",
|
|
79841
80699
|
afterMs,
|
|
79842
80700
|
limit
|
|
@@ -79986,7 +80844,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
79986
80844
|
for (const { event, score } of bestEventByTrackId.values()) {
|
|
79987
80845
|
const mediaUrl = resolveSearchThumbnailUrl({
|
|
79988
80846
|
baseUrl: this.eventMediaBaseUrl,
|
|
79989
|
-
eventId: event.id,
|
|
80847
|
+
eventId: String(event.id),
|
|
79990
80848
|
...event.trackId !== void 0 && embeddingMediaKeyByTrackId.has(event.trackId) ? { embeddingMediaKey: embeddingMediaKeyByTrackId.get(event.trackId) } : {}
|
|
79991
80849
|
});
|
|
79992
80850
|
const keyFrameMediaKey = event.trackId !== void 0 ? keyFrameKeyByTrackId.get(event.trackId) : void 0;
|
|
@@ -80585,12 +81443,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
80585
81443
|
deviceId: file.deviceId
|
|
80586
81444
|
};
|
|
80587
81445
|
};
|
|
80588
|
-
|
|
81446
|
+
let eventFiles = [];
|
|
81447
|
+
for (const ownerType of EVENT_OWNER_TYPES) {
|
|
81448
|
+
eventFiles = await listInfo(ownerType, id);
|
|
81449
|
+
if (eventFiles.length > 0) break;
|
|
81450
|
+
}
|
|
80589
81451
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
80590
81452
|
const layers = await collectCropForcedMedia({
|
|
80591
81453
|
listByOwner: listInfo,
|
|
80592
81454
|
getTrackIdForEvent
|
|
80593
|
-
}, id);
|
|
81455
|
+
}, id, eventFiles);
|
|
80594
81456
|
const bodyKeys = new Set(layers.bodyFiles.map((f) => f.key));
|
|
80595
81457
|
return await readWinningBlob([...layers.bodyFiles, ...layers.detailFiles], (rows) => resolveCropForcedMedia(rows.filter((r) => bodyKeys.has(r.key)), (files) => pickCleanMedia(files, preferKind), rows.filter((r) => !bodyKeys.has(r.key))), readBlob);
|
|
80596
81458
|
}
|