@camstack/addon-post-analysis 1.2.224 → 1.2.226
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-ChXdkiVq.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-DoL6NTW5.mjs → hostInit-B95vfMXS.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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-BypQW8jz.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -16,6 +16,7 @@ sharp = require_dist.__toESM(sharp);
|
|
|
16
16
|
let node_os = require("node:os");
|
|
17
17
|
node_os = require_dist.__toESM(node_os);
|
|
18
18
|
let node_buffer = require("node:buffer");
|
|
19
|
+
let node_zlib = require("node:zlib");
|
|
19
20
|
//#region src/pipeline-analytics/media-occupancy.ts
|
|
20
21
|
/** The bare type ref legacy NULL-stamped media rows belong to. */
|
|
21
22
|
var LEGACY_MEDIA_TYPE = "eventMedia";
|
|
@@ -55,6 +56,183 @@ function mediaOccupancyReports(footprint, locationIds, measuredAtMs) {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
//#endregion
|
|
59
|
+
//#region src/pipeline-analytics/motion-episode.ts
|
|
60
|
+
/**
|
|
61
|
+
* Decide what one `(detected, timestamp)` observation does to the episode
|
|
62
|
+
* state for one `(deviceId, source)` key. `prior` is `undefined` the first
|
|
63
|
+
* time this key is ever observed (fresh process, or a key never seen).
|
|
64
|
+
*/
|
|
65
|
+
function decideMotionSignal(prior, detected, timestamp) {
|
|
66
|
+
if (!detected) {
|
|
67
|
+
if (prior === void 0) return { kind: "noop" };
|
|
68
|
+
return { kind: "observed-off" };
|
|
69
|
+
}
|
|
70
|
+
if (prior?.episode == null) return { kind: "open-episode" };
|
|
71
|
+
const isNewEdge = prior.lastDetected === false;
|
|
72
|
+
const edgeOffsetsMs = isNewEdge ? [...prior.episode.edgeOffsetsMs, timestamp - prior.episode.startedAt] : prior.episode.edgeOffsetsMs;
|
|
73
|
+
return {
|
|
74
|
+
kind: "extend-episode",
|
|
75
|
+
next: {
|
|
76
|
+
...prior.episode,
|
|
77
|
+
lastOnAt: timestamp,
|
|
78
|
+
edgeOffsetsMs
|
|
79
|
+
},
|
|
80
|
+
isNewEdge
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** `lastOnAt - startedAt` — see the module doc for why not `closedAt - startedAt`. */
|
|
84
|
+
function motionEpisodeDurationMs(state) {
|
|
85
|
+
return state.lastOnAt - state.startedAt;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/pipeline-analytics/motion-episode-tracker.ts
|
|
89
|
+
function keyOf(deviceId, source) {
|
|
90
|
+
return `${deviceId}:${source}`;
|
|
91
|
+
}
|
|
92
|
+
var MotionEpisodeTracker = class {
|
|
93
|
+
deps;
|
|
94
|
+
signals = /* @__PURE__ */ new Map();
|
|
95
|
+
timers = /* @__PURE__ */ new Map();
|
|
96
|
+
constructor(deps) {
|
|
97
|
+
this.deps = deps;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One `(detected, timestamp)` observation for one `(deviceId, source)` key.
|
|
101
|
+
* `regionData` is read only if this observation OPENS a new episode — see
|
|
102
|
+
* {@link MotionEpisodeInsertInput}.
|
|
103
|
+
*/
|
|
104
|
+
async observe(deviceId, source, detected, timestamp, regionData) {
|
|
105
|
+
const key = keyOf(deviceId, source);
|
|
106
|
+
const prior = this.signals.get(key);
|
|
107
|
+
const decision = decideMotionSignal(prior, detected, timestamp);
|
|
108
|
+
switch (decision.kind) {
|
|
109
|
+
case "noop": return;
|
|
110
|
+
case "observed-off":
|
|
111
|
+
this.setSignal(key, deviceId, source, false, prior?.episode ?? null);
|
|
112
|
+
return;
|
|
113
|
+
case "open-episode": {
|
|
114
|
+
const eventId = await this.deps.insertRow({
|
|
115
|
+
deviceId,
|
|
116
|
+
source,
|
|
117
|
+
timestamp,
|
|
118
|
+
regionData
|
|
119
|
+
});
|
|
120
|
+
if (eventId === null) {
|
|
121
|
+
this.deps.logger.warn("motion episode open failed — insert did not land", {
|
|
122
|
+
tags: { deviceId },
|
|
123
|
+
meta: {
|
|
124
|
+
source,
|
|
125
|
+
timestamp
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
this.setSignal(key, deviceId, source, true, null);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const episode = {
|
|
132
|
+
eventId,
|
|
133
|
+
startedAt: timestamp,
|
|
134
|
+
lastOnAt: timestamp,
|
|
135
|
+
edgeOffsetsMs: [0]
|
|
136
|
+
};
|
|
137
|
+
this.setSignal(key, deviceId, source, true, episode);
|
|
138
|
+
this.armCloseTimer(key);
|
|
139
|
+
this.deps.onNewEdge?.({
|
|
140
|
+
deviceId,
|
|
141
|
+
source,
|
|
142
|
+
timestamp,
|
|
143
|
+
eventId,
|
|
144
|
+
isFirst: true
|
|
145
|
+
});
|
|
146
|
+
this.deps.onTick?.({
|
|
147
|
+
deviceId,
|
|
148
|
+
source,
|
|
149
|
+
timestamp
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
case "extend-episode":
|
|
154
|
+
this.setSignal(key, deviceId, source, true, decision.next);
|
|
155
|
+
this.armCloseTimer(key);
|
|
156
|
+
if (decision.isNewEdge) this.deps.onNewEdge?.({
|
|
157
|
+
deviceId,
|
|
158
|
+
source,
|
|
159
|
+
timestamp,
|
|
160
|
+
eventId: decision.next.eventId,
|
|
161
|
+
isFirst: false
|
|
162
|
+
});
|
|
163
|
+
this.deps.onTick?.({
|
|
164
|
+
deviceId,
|
|
165
|
+
source,
|
|
166
|
+
timestamp
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Close every currently-open episode immediately, without waiting out the
|
|
173
|
+
* quiet window — used at addon shutdown so a row never sits with
|
|
174
|
+
* `durationMs: null` forever because the process that would have closed it
|
|
175
|
+
* is gone. Each close is logged with `reason` so an operator reading the
|
|
176
|
+
* row's neighbours understands why it stopped short of a natural close.
|
|
177
|
+
*/
|
|
178
|
+
async closeAll(reason) {
|
|
179
|
+
const keys = [...this.signals.keys()];
|
|
180
|
+
for (const key of keys) {
|
|
181
|
+
const signal = this.signals.get(key);
|
|
182
|
+
if (!signal?.episode) continue;
|
|
183
|
+
await this.closeNow(key, signal, reason);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
setSignal(key, deviceId, source, lastDetected, episode) {
|
|
187
|
+
this.signals.set(key, {
|
|
188
|
+
deviceId,
|
|
189
|
+
source,
|
|
190
|
+
lastDetected,
|
|
191
|
+
episode
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
armCloseTimer(key) {
|
|
195
|
+
const existing = this.timers.get(key);
|
|
196
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
197
|
+
const timer = setTimeout(() => {
|
|
198
|
+
this.fireClose(key);
|
|
199
|
+
}, this.deps.closeAfterMs);
|
|
200
|
+
timer.unref?.();
|
|
201
|
+
this.timers.set(key, timer);
|
|
202
|
+
}
|
|
203
|
+
async fireClose(key) {
|
|
204
|
+
this.timers.delete(key);
|
|
205
|
+
const signal = this.signals.get(key);
|
|
206
|
+
if (!signal?.episode) return;
|
|
207
|
+
await this.closeNow(key, signal, "quiet-window-elapsed");
|
|
208
|
+
}
|
|
209
|
+
async closeNow(key, signal, reason) {
|
|
210
|
+
const episode = signal.episode;
|
|
211
|
+
if (!episode) return;
|
|
212
|
+
const timer = this.timers.get(key);
|
|
213
|
+
if (timer !== void 0) {
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
this.timers.delete(key);
|
|
216
|
+
}
|
|
217
|
+
signal.episode = null;
|
|
218
|
+
const durationMs = motionEpisodeDurationMs(episode);
|
|
219
|
+
if (!await this.deps.closeRow({
|
|
220
|
+
deviceId: signal.deviceId,
|
|
221
|
+
source: signal.source,
|
|
222
|
+
eventId: episode.eventId,
|
|
223
|
+
durationMs,
|
|
224
|
+
edges: episode.edgeOffsetsMs
|
|
225
|
+
})) this.deps.logger.warn("motion episode close failed — row keeps durationMs: null", {
|
|
226
|
+
tags: { deviceId: signal.deviceId },
|
|
227
|
+
meta: {
|
|
228
|
+
source: signal.source,
|
|
229
|
+
eventId: episode.eventId,
|
|
230
|
+
reason
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
//#endregion
|
|
58
236
|
//#region src/notification-center/action-token.ts
|
|
59
237
|
/**
|
|
60
238
|
* The authority behind a notification button.
|
|
@@ -6757,7 +6935,7 @@ function normalizeBbox(bbox, frameWidth, frameHeight) {
|
|
|
6757
6935
|
function subjectFromObjectEvent(ev) {
|
|
6758
6936
|
return {
|
|
6759
6937
|
kind: "object-event",
|
|
6760
|
-
recordId: ev.id,
|
|
6938
|
+
recordId: String(ev.id),
|
|
6761
6939
|
deviceId: ev.deviceId,
|
|
6762
6940
|
timestamp: ev.timestamp,
|
|
6763
6941
|
classNames: [ev.className],
|
|
@@ -6825,7 +7003,7 @@ function subjectFromSensorEvent(ev, markerTrackId) {
|
|
|
6825
7003
|
const eventType = readSensorEventType(ev.value);
|
|
6826
7004
|
return {
|
|
6827
7005
|
kind: "device-event",
|
|
6828
|
-
recordId: ev.id,
|
|
7006
|
+
recordId: String(ev.id),
|
|
6829
7007
|
deviceId: ev.deviceId,
|
|
6830
7008
|
...markerTrackId !== void 0 ? { trackId: markerTrackId } : {},
|
|
6831
7009
|
...ev.sourceDeviceId !== ev.deviceId ? { sourceDeviceId: ev.sourceDeviceId } : {},
|
|
@@ -6862,7 +7040,7 @@ function subjectFromAudioEvent(ev, stillOwnerId) {
|
|
|
6862
7040
|
const macro = ev.classification?.className;
|
|
6863
7041
|
return {
|
|
6864
7042
|
kind: "audio-event",
|
|
6865
|
-
recordId: ev.id,
|
|
7043
|
+
recordId: String(ev.id),
|
|
6866
7044
|
deviceId: ev.deviceId,
|
|
6867
7045
|
timestamp: ev.timestamp,
|
|
6868
7046
|
classNames: macro !== void 0 ? [`audio-${macro}`] : [],
|
|
@@ -6992,7 +7170,7 @@ function occupancySubjectFromEdge(edge) {
|
|
|
6992
7170
|
function subjectFromPackageEvent(ev, phase) {
|
|
6993
7171
|
return {
|
|
6994
7172
|
kind: "package-event",
|
|
6995
|
-
recordId: ev.id,
|
|
7173
|
+
recordId: String(ev.id),
|
|
6996
7174
|
deviceId: ev.deviceId,
|
|
6997
7175
|
timestamp: ev.timestamp,
|
|
6998
7176
|
classNames: [ev.className],
|
|
@@ -11674,7 +11852,7 @@ var NcDispatcher = class {
|
|
|
11674
11852
|
recordId: entry.recordId,
|
|
11675
11853
|
policy: entry.payload.media,
|
|
11676
11854
|
...entry.payload.mediaFrame !== void 0 ? { frame: entry.payload.mediaFrame } : {},
|
|
11677
|
-
owners: [...entry.payload.subject.eventId !== void 0 ? [`
|
|
11855
|
+
owners: [...entry.payload.subject.eventId !== void 0 ? [`object:${entry.payload.subject.eventId}`] : [], ...entry.payload.subject.trackId !== void 0 ? [`track:${entry.payload.subject.trackId}`] : []]
|
|
11678
11856
|
}
|
|
11679
11857
|
});
|
|
11680
11858
|
out.push(...footage.attachments);
|
|
@@ -11981,7 +12159,7 @@ var NcDispatcher = class {
|
|
|
11981
12159
|
const signal = policy === "best-matching" ? matchSignal(entry.payload.matchedOn) : null;
|
|
11982
12160
|
const owners = [];
|
|
11983
12161
|
if (subject.eventId !== void 0) owners.push({
|
|
11984
|
-
kind: "
|
|
12162
|
+
kind: "object",
|
|
11985
12163
|
id: subject.eventId
|
|
11986
12164
|
});
|
|
11987
12165
|
if (subject.trackId !== void 0) owners.push({
|
|
@@ -28216,16 +28394,28 @@ var StoragePressureTracker = class {
|
|
|
28216
28394
|
* operator-captured reference picture as collectable. It is the same class of
|
|
28217
28395
|
* mistake `identity` and `vehicle` are exempt for: a blob the OPERATOR curated,
|
|
28218
28396
|
* which nothing else can regenerate and no cascade owns.
|
|
28397
|
+
*
|
|
28398
|
+
* `event` was DROPPED and replaced by `motion` / `object` / `audio` (D476).
|
|
28399
|
+
* `event` used to cover all three event tables under one owner kind, which was
|
|
28400
|
+
* safe while event ids were UUIDs (unique across tables). D474 made an event
|
|
28401
|
+
* id a per-table SQLite rowid alias, so `event:5` stopped naming one row —
|
|
28402
|
+
* three different rows, one per table, can all be `5`. This is the AUTHORITY
|
|
28403
|
+
* on what may own media; `MEDIA_OWNER_TYPES` in
|
|
28404
|
+
* `@camstack/types/media/media-owner-key.ts` must cover every member here —
|
|
28405
|
+
* `scripts` / `__tests__/owner-kinds-authority.spec.ts` guards the two lists
|
|
28406
|
+
* staying married.
|
|
28219
28407
|
*/
|
|
28220
28408
|
var OWNER_KINDS = [
|
|
28221
|
-
"event",
|
|
28222
28409
|
"track",
|
|
28223
28410
|
"summary",
|
|
28224
28411
|
"face",
|
|
28225
28412
|
"identity",
|
|
28226
28413
|
"plate",
|
|
28227
28414
|
"vehicle",
|
|
28228
|
-
"scene"
|
|
28415
|
+
"scene",
|
|
28416
|
+
"motion",
|
|
28417
|
+
"object",
|
|
28418
|
+
"audio"
|
|
28229
28419
|
];
|
|
28230
28420
|
/**
|
|
28231
28421
|
* Owner kinds that NOTHING may write any more, but whose rows can still be on
|
|
@@ -28622,7 +28812,12 @@ function mediaRowLocation(data) {
|
|
|
28622
28812
|
return typeof id === "string" && id.length > 0 ? id : DEFAULT_MEDIA_LOCATION;
|
|
28623
28813
|
}
|
|
28624
28814
|
function buildKey(params) {
|
|
28625
|
-
return
|
|
28815
|
+
return require_dist.formatMediaOwnerKey({
|
|
28816
|
+
ownerType: params.ownerKind,
|
|
28817
|
+
ownerId: params.ownerId,
|
|
28818
|
+
fileKind: params.kind,
|
|
28819
|
+
...isSingleInstanceKind(params.kind) ? {} : { timestampMs: params.timestamp }
|
|
28820
|
+
});
|
|
28626
28821
|
}
|
|
28627
28822
|
function buildPath(params) {
|
|
28628
28823
|
const base = `${params.deviceId}/events/${params.ownerKind}/${params.ownerId}`;
|
|
@@ -29537,9 +29732,27 @@ var MediaStore = class {
|
|
|
29537
29732
|
}
|
|
29538
29733
|
return newKey;
|
|
29539
29734
|
}
|
|
29540
|
-
/**
|
|
29541
|
-
|
|
29542
|
-
|
|
29735
|
+
/**
|
|
29736
|
+
* Delete event-owned media for evicted events, GROUPED by the table each id
|
|
29737
|
+
* came from (D476).
|
|
29738
|
+
*
|
|
29739
|
+
* There is no honest single-kind signature any more: `deleteForOwner` needs
|
|
29740
|
+
* ONE owner kind per call, and a flat id list can no longer say which of
|
|
29741
|
+
* `motion` / `object` / `audio` an id names — D474 made event ids per-table
|
|
29742
|
+
* rowids, so `motion:5` and `object:5` are different rows that both spell
|
|
29743
|
+
* `5`. Every caller (the retention cascade's per-track object-event sweep,
|
|
29744
|
+
* the trackless motion/audio sweep, the operator prune) already KNOWS which
|
|
29745
|
+
* table produced each id before it reaches here — the old flat signature
|
|
29746
|
+
* just threw that fact away. `ids` is a partial record so a caller supplies
|
|
29747
|
+
* only the kinds it actually collected.
|
|
29748
|
+
*/
|
|
29749
|
+
async deleteForEvents(ids) {
|
|
29750
|
+
let total = 0;
|
|
29751
|
+
for (const ownerType of require_dist.EVENT_OWNER_TYPES) {
|
|
29752
|
+
const kindIds = ids[ownerType];
|
|
29753
|
+
if (kindIds !== void 0 && kindIds.length > 0) total += await this.deleteForOwner(ownerType, kindIds);
|
|
29754
|
+
}
|
|
29755
|
+
return total;
|
|
29543
29756
|
}
|
|
29544
29757
|
/** Delete all media owned by these tracks (keyFrame/snapshot/thumbnail). */
|
|
29545
29758
|
async deleteForTracks(trackIds) {
|
|
@@ -29554,8 +29767,9 @@ var MediaStore = class {
|
|
|
29554
29767
|
* ENROLLED face/plate has its crop REOWNED to `identity`/`vehicle` at
|
|
29555
29768
|
* assignment, so deleting only the `'face'`/`'plate'` owner removes unassigned
|
|
29556
29769
|
* crops and leaves the enrolled gallery intact. Event-owned media
|
|
29557
|
-
* (`ownerKind:'
|
|
29558
|
-
*
|
|
29770
|
+
* (`ownerKind:'motion'` / `'object'` / `'audio'`) is handled by `EventStore`
|
|
29771
|
+
* (which holds the per-kind event id sets — see {@link deleteForEvents}).
|
|
29772
|
+
* `deleteForOwner` is best-effort per row, so one bad row can't abort the
|
|
29559
29773
|
* batch.
|
|
29560
29774
|
*/
|
|
29561
29775
|
async deleteByTracks(trackIds) {
|
|
@@ -36421,23 +36635,6 @@ function ruleApplies(resolved, det, className, maskInfo, _zones, frameWidth, fra
|
|
|
36421
36635
|
}
|
|
36422
36636
|
//#endregion
|
|
36423
36637
|
//#region src/pipeline-analytics/pipeline/frame-processor.ts
|
|
36424
|
-
/**
|
|
36425
|
-
* FrameProcessor — per-device pipeline stage runner.
|
|
36426
|
-
*
|
|
36427
|
-
* Consumes a raw `PipelineInferenceResult` payload and runs:
|
|
36428
|
-
* 1. Flatten first-level detections
|
|
36429
|
-
* 2. Zone filter (ZoneEngine)
|
|
36430
|
-
* 3. Tracking (SortTracker)
|
|
36431
|
-
* 4. State analysis (StateAnalyzer)
|
|
36432
|
-
* 5. Event emission (DetectionEventEmitter)
|
|
36433
|
-
*
|
|
36434
|
-
* Returns a structured result the addon uses to:
|
|
36435
|
-
* - upsert TrackStore
|
|
36436
|
-
* - insert ObjectEvent rows into the declared collection
|
|
36437
|
-
* - emit onFrameTracked / onDetectionEvent on the bus
|
|
36438
|
-
*
|
|
36439
|
-
* One FrameProcessor per device — lifecycle managed by DeviceRegistry.
|
|
36440
|
-
*/
|
|
36441
36638
|
/** Shared empty — a fold is rare and every other frame would allocate. */
|
|
36442
36639
|
var EMPTY_RIDER_FOLDS = [];
|
|
36443
36640
|
/** Mapping from StateAnalyzer's `ObjectState.state` values to the
|
|
@@ -37173,7 +37370,6 @@ var FrameProcessor = class {
|
|
|
37173
37370
|
const crossing = crossingOf(e);
|
|
37174
37371
|
const labelPatch = labelPatchByTrack.get(e.detection.trackId) ?? knownTrackLabelPatch(this.trackLabelState?.(e.detection.trackId));
|
|
37175
37372
|
return {
|
|
37176
|
-
id: (0, node_crypto.randomUUID)(),
|
|
37177
37373
|
deviceId: this.deviceId,
|
|
37178
37374
|
timestamp,
|
|
37179
37375
|
kind: "object",
|
|
@@ -37910,7 +38106,7 @@ async function cascadeDeleteTrack(stores, trackId) {
|
|
|
37910
38106
|
for (const id of ids) {
|
|
37911
38107
|
const eventIds = await stores.eventStore.deleteByTrack(id);
|
|
37912
38108
|
events += eventIds.length;
|
|
37913
|
-
if (eventIds.length > 0) media += await stores.mediaStore.deleteForEvents([...eventIds]);
|
|
38109
|
+
if (eventIds.length > 0) media += await stores.mediaStore.deleteForEvents({ object: [...eventIds] });
|
|
37914
38110
|
}
|
|
37915
38111
|
return [{
|
|
37916
38112
|
family: "events",
|
|
@@ -39867,6 +40063,11 @@ function runFrameProcessorPass(input) {
|
|
|
39867
40063
|
* remaining blob are dropped — including snapshot-less rows. Enrolled gallery
|
|
39868
40064
|
* media (`identity` / `vehicle` / `scene`) is never probed.
|
|
39869
40065
|
*/
|
|
40066
|
+
/** Is this an event-owned media row — `motion` / `object` / `audio` (D476;
|
|
40067
|
+
* `event` used to cover all three under one owner kind). */
|
|
40068
|
+
function isEventOwnedMediaRow(ownerKind) {
|
|
40069
|
+
return require_dist.EVENT_OWNER_TYPES.includes(ownerKind);
|
|
40070
|
+
}
|
|
39870
40071
|
function shouldProbeMediaRow(ownerKind) {
|
|
39871
40072
|
if (!isMediaOwnerKind(ownerKind)) return true;
|
|
39872
40073
|
return !isRetentionExemptOwnerKind(ownerKind);
|
|
@@ -39897,7 +40098,7 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
39897
40098
|
if (row.path.length === 0 ? false : await deps.blobExists(row.location, row.path)) {
|
|
39898
40099
|
const trackId = trackIdFromMediaOwner(row.ownerKind, row.ownerId);
|
|
39899
40100
|
if (trackId !== null) tracksWithMedia.add(trackId);
|
|
39900
|
-
if (row.ownerKind
|
|
40101
|
+
if (isEventOwnedMediaRow(row.ownerKind)) eventsWithMedia.add(row.ownerId);
|
|
39901
40102
|
continue;
|
|
39902
40103
|
}
|
|
39903
40104
|
missingKeys.push(row.key);
|
|
@@ -39958,6 +40159,191 @@ var locationDiscardActions = require_dist.defineCustomActions({
|
|
|
39958
40159
|
"storage.discardLocationCancel": require_dist.customAction(require_dist.object({}), DiscardCancel, { auth: "admin" })
|
|
39959
40160
|
});
|
|
39960
40161
|
//#endregion
|
|
40162
|
+
//#region src/pipeline-analytics/store/motion-edge-codec.ts
|
|
40163
|
+
/**
|
|
40164
|
+
* How a motion episode's rising-edge offsets become bytes, and back (D475).
|
|
40165
|
+
*
|
|
40166
|
+
* A motion episode row (`durationMs` + `edges`) records every distinct
|
|
40167
|
+
* off→on transition the camera reported while the episode stayed open — "ogni
|
|
40168
|
+
* evento on si deve salvare" (the operator's words). `edges` holds those
|
|
40169
|
+
* instants as **ms offsets from the episode's `timestamp`** (its first rising
|
|
40170
|
+
* edge, so the first entry is always `0`), never as absolute epochs — an
|
|
40171
|
+
* epoch is ~13 digits per entry; an offset into a 40 s episode is 1–5.
|
|
40172
|
+
*
|
|
40173
|
+
* Mirrors `position-codec.ts` (D471) on purpose: same discipline, different
|
|
40174
|
+
* shape. A MALFORMED stored value decodes to an empty array, never a throw —
|
|
40175
|
+
* one row's garbage `edges` column must not take out the whole event read
|
|
40176
|
+
* path, and a legacy row (written before this column existed) has no `edges`
|
|
40177
|
+
* at all, which decodes to `[]` exactly like garbage does. The open/closed
|
|
40178
|
+
* question is answered by `durationMs` being `null` vs a number vs absent —
|
|
40179
|
+
* never by this column, so an empty `edges` array must never be read as "no
|
|
40180
|
+
* episode" or "still open".
|
|
40181
|
+
*/
|
|
40182
|
+
/**
|
|
40183
|
+
* Marks a value this module wrote. A distinct namespace from
|
|
40184
|
+
* `position-codec.ts`'s `z1:` — different column, different shape, so a
|
|
40185
|
+
* value from one can never be mistaken for the other's.
|
|
40186
|
+
*/
|
|
40187
|
+
var ENCODED_PREFIX$1 = "me1:";
|
|
40188
|
+
/** Round-trip a sequence of rising-edge offsets (ms, from the episode start)
|
|
40189
|
+
* through gzip + base64 — the same trade `position-codec.ts` made: base64
|
|
40190
|
+
* because the settings-store column types have no BLOB, gzip because the
|
|
40191
|
+
* repeated small integers compress well. */
|
|
40192
|
+
function encodeMotionEdges(edgesMs) {
|
|
40193
|
+
const json = JSON.stringify(edgesMs);
|
|
40194
|
+
return ENCODED_PREFIX$1 + (0, node_zlib.gzipSync)(Buffer.from(json, "utf8"), { level: 6 }).toString("base64");
|
|
40195
|
+
}
|
|
40196
|
+
/**
|
|
40197
|
+
* Read a stored `edges` value. Two shapes reach here: a `me1:` string this
|
|
40198
|
+
* module wrote, and a plain array (the store parsed the JSON column for us,
|
|
40199
|
+
* or a caller handed one straight through — the codec should not be the
|
|
40200
|
+
* reason a valid in-memory array fails to read back). Anything else —
|
|
40201
|
+
* `undefined` (legacy row, or an episode that never closed), a garbage
|
|
40202
|
+
* string, a number — answers `[]`.
|
|
40203
|
+
*/
|
|
40204
|
+
function decodeMotionEdges(value) {
|
|
40205
|
+
if (Array.isArray(value)) return value.filter(isFiniteNumber);
|
|
40206
|
+
if (typeof value !== "string") return [];
|
|
40207
|
+
if (value.startsWith(ENCODED_PREFIX$1)) try {
|
|
40208
|
+
const raw = (0, node_zlib.gunzipSync)(Buffer.from(value.slice(4), "base64")).toString("utf8");
|
|
40209
|
+
const parsed = JSON.parse(raw);
|
|
40210
|
+
return Array.isArray(parsed) ? parsed.filter(isFiniteNumber) : [];
|
|
40211
|
+
} catch {
|
|
40212
|
+
return [];
|
|
40213
|
+
}
|
|
40214
|
+
try {
|
|
40215
|
+
const parsed = JSON.parse(value);
|
|
40216
|
+
return Array.isArray(parsed) ? parsed.filter(isFiniteNumber) : [];
|
|
40217
|
+
} catch {
|
|
40218
|
+
return [];
|
|
40219
|
+
}
|
|
40220
|
+
}
|
|
40221
|
+
function isFiniteNumber(v) {
|
|
40222
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
40223
|
+
}
|
|
40224
|
+
//#endregion
|
|
40225
|
+
//#region src/pipeline-analytics/store/position-codec.ts
|
|
40226
|
+
/**
|
|
40227
|
+
* How a track's `positions[]` becomes bytes, and back (D471).
|
|
40228
|
+
*
|
|
40229
|
+
* `positions` was 94.9 % of the `tracks` table and 17 % of the whole settings
|
|
40230
|
+
* database — 186.5 MB — and most of that was not information. See
|
|
40231
|
+
* `__tests__/position-codec.spec.ts` for the measurement and for why rounding
|
|
40232
|
+
* is safe only since D399.
|
|
40233
|
+
*
|
|
40234
|
+
* Pure on purpose: what a MALFORMED stored value decodes to is the decision
|
|
40235
|
+
* worth pinning in a test, and it is "an empty trail", never a throw.
|
|
40236
|
+
*/
|
|
40237
|
+
/**
|
|
40238
|
+
* Marks a value this module wrote. Chosen so it cannot collide with the plain
|
|
40239
|
+
* JSON array every pre-D471 row holds: that always starts `[`.
|
|
40240
|
+
*/
|
|
40241
|
+
var ENCODED_PREFIX = "z1:";
|
|
40242
|
+
/** Pixels are integers. The float tail is representation noise, not precision. */
|
|
40243
|
+
function roundPosition(p) {
|
|
40244
|
+
return {
|
|
40245
|
+
...p,
|
|
40246
|
+
x: Math.round(p.x),
|
|
40247
|
+
y: Math.round(p.y),
|
|
40248
|
+
bbox: {
|
|
40249
|
+
...p.bbox,
|
|
40250
|
+
x: Math.round(p.bbox.x),
|
|
40251
|
+
y: Math.round(p.bbox.y),
|
|
40252
|
+
w: Math.round(p.bbox.w),
|
|
40253
|
+
h: Math.round(p.bbox.h)
|
|
40254
|
+
}
|
|
40255
|
+
};
|
|
40256
|
+
}
|
|
40257
|
+
/** Round, serialise, gzip, base64 — in that order, so the compressor sees the
|
|
40258
|
+
* smaller text and the repeated field names it compresses best. */
|
|
40259
|
+
function encodePositions(positions) {
|
|
40260
|
+
const json = JSON.stringify(positions.map(roundPosition));
|
|
40261
|
+
return ENCODED_PREFIX + (0, node_zlib.gzipSync)(Buffer.from(json, "utf8"), { level: 6 }).toString("base64");
|
|
40262
|
+
}
|
|
40263
|
+
/**
|
|
40264
|
+
* Read a stored value, whichever era wrote it.
|
|
40265
|
+
*
|
|
40266
|
+
* THREE shapes reach here and all three are ordinary, which is what makes a
|
|
40267
|
+
* migration unnecessary: a `z1:` string this module wrote, a plain array (the
|
|
40268
|
+
* store parsed the JSON column for us), and a plain JSON string (it did not).
|
|
40269
|
+
* Anything else is a row we cannot read, and the answer is an empty trail —
|
|
40270
|
+
* the timeline then draws a track with no path, which an operator can SEE,
|
|
40271
|
+
* instead of failing the page, which they cannot.
|
|
40272
|
+
*/
|
|
40273
|
+
function decodePositions(value) {
|
|
40274
|
+
if (Array.isArray(value)) return value.filter(isPosition);
|
|
40275
|
+
if (typeof value !== "string") return [];
|
|
40276
|
+
if (value.startsWith(ENCODED_PREFIX)) try {
|
|
40277
|
+
const raw = (0, node_zlib.gunzipSync)(Buffer.from(value.slice(3), "base64")).toString("utf8");
|
|
40278
|
+
const parsed = JSON.parse(raw);
|
|
40279
|
+
return Array.isArray(parsed) ? parsed.filter(isPosition) : [];
|
|
40280
|
+
} catch {
|
|
40281
|
+
return [];
|
|
40282
|
+
}
|
|
40283
|
+
try {
|
|
40284
|
+
const parsed = JSON.parse(value);
|
|
40285
|
+
return Array.isArray(parsed) ? parsed.filter(isPosition) : [];
|
|
40286
|
+
} catch {
|
|
40287
|
+
return [];
|
|
40288
|
+
}
|
|
40289
|
+
}
|
|
40290
|
+
function isPosition(v) {
|
|
40291
|
+
if (typeof v !== "object" || v === null) return false;
|
|
40292
|
+
const p = { ...v };
|
|
40293
|
+
return typeof p["x"] === "number" && typeof p["y"] === "number" && typeof p["bbox"] === "object";
|
|
40294
|
+
}
|
|
40295
|
+
/**
|
|
40296
|
+
* A trajectory reduced to the ONLY thing a zone-filtered reader uses (D471).
|
|
40297
|
+
*
|
|
40298
|
+
* The client's `parsePositionBoxes` walks `positions[]` and keeps `p.bbox`,
|
|
40299
|
+
* discarding `x`, `y`, `timestamp` and `source` the moment they arrive. On the
|
|
40300
|
+
* zone path — the one path that ships positions at all — that is **59 % of the
|
|
40301
|
+
* payload thrown away by the receiver**: 162 B per position sent, 66 B used.
|
|
40302
|
+
*
|
|
40303
|
+
* Measured on 20 130 real positions. This is the reduction, applied server-side
|
|
40304
|
+
* so the bytes are never spent.
|
|
40305
|
+
*
|
|
40306
|
+
* NOT applied to any other read: a caller that asked for a full trajectory gets
|
|
40307
|
+
* one. This is narrowing on the request that already narrowed.
|
|
40308
|
+
*/
|
|
40309
|
+
function geometryOnlyPositions(positions) {
|
|
40310
|
+
return positions.map((p) => ({
|
|
40311
|
+
x: p.bbox.x,
|
|
40312
|
+
y: p.bbox.y,
|
|
40313
|
+
timestamp: 0,
|
|
40314
|
+
bbox: p.bbox
|
|
40315
|
+
}));
|
|
40316
|
+
}
|
|
40317
|
+
/**
|
|
40318
|
+
* A detection box with integer pixel coordinates (D471).
|
|
40319
|
+
*
|
|
40320
|
+
* `object-events.bbox` carries the same float tail as `positions` did —
|
|
40321
|
+
* `{"w":1089.6000000000001,"h":398.40000000000003}` — and is 18 % of a 30 MB
|
|
40322
|
+
* table. Same cause: a pixel scaled by the analysis raster ratio, serialised
|
|
40323
|
+
* with every digit JavaScript can print.
|
|
40324
|
+
*
|
|
40325
|
+
* Returns the input unchanged when there is nothing to round, so a legacy row
|
|
40326
|
+
* with no bbox stays exactly what it was.
|
|
40327
|
+
*/
|
|
40328
|
+
function roundBbox(bbox) {
|
|
40329
|
+
if (typeof bbox !== "object" || bbox === null) return bbox;
|
|
40330
|
+
const b = { ...bbox };
|
|
40331
|
+
let touched = false;
|
|
40332
|
+
for (const k of [
|
|
40333
|
+
"x",
|
|
40334
|
+
"y",
|
|
40335
|
+
"w",
|
|
40336
|
+
"h"
|
|
40337
|
+
]) {
|
|
40338
|
+
const v = b[k];
|
|
40339
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
40340
|
+
b[k] = Math.round(v);
|
|
40341
|
+
touched = true;
|
|
40342
|
+
}
|
|
40343
|
+
}
|
|
40344
|
+
return touched ? b : bbox;
|
|
40345
|
+
}
|
|
40346
|
+
//#endregion
|
|
39961
40347
|
//#region src/pipeline-analytics/store/tiered-label-columns.ts
|
|
39962
40348
|
/**
|
|
39963
40349
|
* The six tiered-label columns, read and written in ONE place (roadmap 4g).
|
|
@@ -40078,12 +40464,17 @@ function audioDbfsByBucket(events, since, bucketMs) {
|
|
|
40078
40464
|
//#region src/pipeline-analytics/store/event-store.ts
|
|
40079
40465
|
/**
|
|
40080
40466
|
* @durable class=ledger owner=pipeline-analytics
|
|
40081
|
-
* write="one row
|
|
40082
|
-
*
|
|
40083
|
-
*
|
|
40084
|
-
*
|
|
40467
|
+
* write="one row per motion EPISODE, keyed per (deviceId, source) so the
|
|
40468
|
+
* analyzer and a firmware/device-activity sensor on the SAME camera never
|
|
40469
|
+
* share one episode (D475). INSERT on a rising edge with no episode open
|
|
40470
|
+
* (durationMs: null, edges: [0]); a further rising edge inside the open
|
|
40471
|
+
* episode does NOT insert — it extends the in-memory episode and re-arms
|
|
40472
|
+
* the MOTION_CLOSE_AFTER_MS close timer. One UPDATE at close writes the
|
|
40473
|
+
* final durationMs (lastOnAt - startedAt) and the encoded edges array.
|
|
40474
|
+
* Replaces the pre-D475 scheme (one row off→on, then a heartbeat row
|
|
40475
|
+
* every 5 s while motion stayed on) — MOTION_EVENT_HEARTBEAT_MS is gone."
|
|
40085
40476
|
* retention="a TERMINAL age-keyed root — no trackId. A motion row may own a
|
|
40086
|
-
* still (`ownerKind: '
|
|
40477
|
+
* still (`ownerKind: 'motion'`, `kind: 'snapshot'`) captured on the off→on
|
|
40087
40478
|
* visit so events-mode recordings have a videoclip thumbnail. Deleted by
|
|
40088
40479
|
* EventStore.evictTracklessBefore on the DEVICE's one retention window
|
|
40089
40480
|
* (follow-recordings horizon, or trackRetentionDays, default 7), which
|
|
@@ -40118,7 +40509,7 @@ var AUDIO_EVENTS_COLLECTION = "pipeline-analytics:audio-events";
|
|
|
40118
40509
|
var COMMON_BASE_COLUMNS = [
|
|
40119
40510
|
{
|
|
40120
40511
|
name: "id",
|
|
40121
|
-
type: "
|
|
40512
|
+
type: "INTEGER",
|
|
40122
40513
|
primaryKey: true,
|
|
40123
40514
|
notNull: true
|
|
40124
40515
|
},
|
|
@@ -40151,7 +40542,31 @@ var MOTION_COLUMNS = [
|
|
|
40151
40542
|
{
|
|
40152
40543
|
name: "frameHeight",
|
|
40153
40544
|
type: "INTEGER"
|
|
40154
|
-
}
|
|
40545
|
+
},
|
|
40546
|
+
(
|
|
40547
|
+
/**
|
|
40548
|
+
* `null` while the episode this row represents is still open; the final
|
|
40549
|
+
* `lastOnAt - startedAt` once closed (D475). DECLARED, not merely stamped —
|
|
40550
|
+
* the sqlite backend silently drops an undeclared key on every write, which
|
|
40551
|
+
* is exactly how `frameId` was written into the void for years (see
|
|
40552
|
+
* `OBJECT_COLUMNS.frameId`). Absent on a row written before D475: that
|
|
40553
|
+
* means "closed the old way", never "open" — only an explicit `null` here
|
|
40554
|
+
* means open.
|
|
40555
|
+
*/
|
|
40556
|
+
{
|
|
40557
|
+
name: "durationMs",
|
|
40558
|
+
type: "INTEGER"
|
|
40559
|
+
}),
|
|
40560
|
+
(
|
|
40561
|
+
/**
|
|
40562
|
+
* Encoded rising-edge offsets for the episode — see `motion-edge-codec.ts`.
|
|
40563
|
+
* Written once, in the same UPDATE as `durationMs`, at close; a legacy or
|
|
40564
|
+
* still-open row has no value here, which the codec reads as `[]`.
|
|
40565
|
+
*/
|
|
40566
|
+
{
|
|
40567
|
+
name: "edges",
|
|
40568
|
+
type: "TEXT"
|
|
40569
|
+
})
|
|
40155
40570
|
];
|
|
40156
40571
|
var OBJECT_COLUMNS = [
|
|
40157
40572
|
...COMMON_BASE_COLUMNS,
|
|
@@ -40177,6 +40592,18 @@ var OBJECT_COLUMNS = [
|
|
|
40177
40592
|
name: "frameId",
|
|
40178
40593
|
type: "TEXT"
|
|
40179
40594
|
}),
|
|
40595
|
+
(
|
|
40596
|
+
/**
|
|
40597
|
+
* The producer's own idempotence key (D474) — see `ObjectEvent.idempotencyKey`.
|
|
40598
|
+
*
|
|
40599
|
+
* DECLARED, not merely stamped: the sqlite backend drops an undeclared key
|
|
40600
|
+
* without error, which is exactly how `frameId` was written into the void on
|
|
40601
|
+
* every row ever persisted. Nullable — only the package detector sets it.
|
|
40602
|
+
*/
|
|
40603
|
+
{
|
|
40604
|
+
name: "idempotencyKey",
|
|
40605
|
+
type: "TEXT"
|
|
40606
|
+
}),
|
|
40180
40607
|
{
|
|
40181
40608
|
name: "trackId",
|
|
40182
40609
|
type: "TEXT",
|
|
@@ -40310,6 +40737,42 @@ var COMMON_INDEXES = (prefix) => [{
|
|
|
40310
40737
|
name: `idx_${prefix}_device_ts`,
|
|
40311
40738
|
columns: ["deviceId", "timestamp"]
|
|
40312
40739
|
}];
|
|
40740
|
+
/**
|
|
40741
|
+
* The wire value `durationMs` carries for an OPEN episode (D475).
|
|
40742
|
+
*
|
|
40743
|
+
* The app-level contract is "`null` = open, a number = closed, absent = a
|
|
40744
|
+
* row from before this column existed" — but SQL has no way to tell "this
|
|
40745
|
+
* column was never given a value because the row predates it" from
|
|
40746
|
+
* "explicitly set to NULL": `declareCollection` adds the column via ALTER
|
|
40747
|
+
* TABLE, which backfills every existing row's `durationMs` with a real SQL
|
|
40748
|
+
* NULL, bit-for-bit the same NULL an open episode would write. Writing an
|
|
40749
|
+
* actual NULL for "open" would therefore make a brand-new open episode
|
|
40750
|
+
* indistinguishable from a five-year-old legacy row — exactly the failure
|
|
40751
|
+
* mode this constant exists to avoid. `insertMotion` writes this in place of
|
|
40752
|
+
* `null`; `queryMotion` translates it back to `null` on the way out, so
|
|
40753
|
+
* nothing outside this file ever sees the sentinel. `-1` is safe because a
|
|
40754
|
+
* real duration is never negative.
|
|
40755
|
+
*/
|
|
40756
|
+
var OPEN_EPISODE_SENTINEL = -1;
|
|
40757
|
+
/**
|
|
40758
|
+
* The key `insert` reports, narrowed to the ROWID these three collections use.
|
|
40759
|
+
*
|
|
40760
|
+
* `settingsStore.insert` answers `string | number` because the SAME method
|
|
40761
|
+
* serves TEXT-keyed collections, where it mints a UUID (D474). On the event
|
|
40762
|
+
* collections the primary key is an `INTEGER` rowid alias, so a string here is
|
|
40763
|
+
* not an event id at all — it means the table in front of us still has the old
|
|
40764
|
+
* TEXT key. Returning `null` says "no id", which is what every caller already
|
|
40765
|
+
* handles; the WARN says why, because a silent null would read as a failed
|
|
40766
|
+
* write (D474).
|
|
40767
|
+
*/
|
|
40768
|
+
function eventRowId(logger, operation, deviceId, key) {
|
|
40769
|
+
if (typeof key === "number") return key;
|
|
40770
|
+
logger.warn(`${operation}: event key is not an INTEGER rowid`, {
|
|
40771
|
+
tags: { deviceId },
|
|
40772
|
+
meta: { returnedId: key }
|
|
40773
|
+
});
|
|
40774
|
+
return null;
|
|
40775
|
+
}
|
|
40313
40776
|
var EventStore = class {
|
|
40314
40777
|
store;
|
|
40315
40778
|
logger;
|
|
@@ -40348,65 +40811,129 @@ var EventStore = class {
|
|
|
40348
40811
|
indexes: COMMON_INDEXES("audio")
|
|
40349
40812
|
});
|
|
40350
40813
|
}
|
|
40814
|
+
/**
|
|
40815
|
+
* Returns the ROWID SQLite assigned, or `null` when the write failed (D474).
|
|
40816
|
+
*
|
|
40817
|
+
* The caller needs it on the very next line — the notification centre reads
|
|
40818
|
+
* it and the marker-still path builds a media key from it — and with an
|
|
40819
|
+
* auto-assigned key the insert is the ONLY place it is knowable. A
|
|
40820
|
+
* placeholder would reach the event bus and the media keys.
|
|
40821
|
+
*/
|
|
40351
40822
|
async insertMotion(ev) {
|
|
40352
40823
|
this.onMotionSample?.(ev.deviceId, ev.regionCount, ev.timestamp);
|
|
40824
|
+
const record = { ...ev };
|
|
40825
|
+
if (ev.durationMs === null) record["durationMs"] = OPEN_EPISODE_SENTINEL;
|
|
40353
40826
|
try {
|
|
40354
|
-
const
|
|
40355
|
-
await this.store.insert.mutate({
|
|
40827
|
+
const inserted = await this.store.insert.mutate({
|
|
40356
40828
|
collection: MOTION_EVENTS_COLLECTION,
|
|
40357
|
-
record: {
|
|
40358
|
-
id,
|
|
40359
|
-
data: rest
|
|
40360
|
-
}
|
|
40829
|
+
record: { data: record }
|
|
40361
40830
|
});
|
|
40831
|
+
return eventRowId(this.logger, "insertMotion", ev.deviceId, inserted.id);
|
|
40362
40832
|
} catch (err) {
|
|
40363
40833
|
this.logger.warn("insertMotion failed", {
|
|
40364
40834
|
tags: { deviceId: ev.deviceId },
|
|
40365
40835
|
meta: {
|
|
40366
|
-
|
|
40836
|
+
timestamp: ev.timestamp,
|
|
40837
|
+
error: String(err)
|
|
40838
|
+
}
|
|
40839
|
+
});
|
|
40840
|
+
return null;
|
|
40841
|
+
}
|
|
40842
|
+
}
|
|
40843
|
+
/**
|
|
40844
|
+
* The one place a motion EPISODE row is closed (D475). Writes `durationMs`
|
|
40845
|
+
* and the encoded `edges` in a single UPDATE — a row never gets a second
|
|
40846
|
+
* write after this one. Returns whether it landed; the caller
|
|
40847
|
+
* (`MotionEpisodeTracker`) logs the failure with the episode's `deviceId`
|
|
40848
|
+
* tag, because a `durationMs: null` row that never gets a reason is exactly
|
|
40849
|
+
* the silent-forever-open shape D475 exists to prevent.
|
|
40850
|
+
*/
|
|
40851
|
+
async updateMotion(eventId, deviceId, patch) {
|
|
40852
|
+
try {
|
|
40853
|
+
await this.store.update.mutate({
|
|
40854
|
+
collection: MOTION_EVENTS_COLLECTION,
|
|
40855
|
+
id: String(eventId),
|
|
40856
|
+
data: {
|
|
40857
|
+
durationMs: patch.durationMs,
|
|
40858
|
+
edges: encodeMotionEdges(patch.edges)
|
|
40859
|
+
}
|
|
40860
|
+
});
|
|
40861
|
+
return true;
|
|
40862
|
+
} catch (err) {
|
|
40863
|
+
this.logger.warn("updateMotion (episode close) failed", {
|
|
40864
|
+
tags: { deviceId },
|
|
40865
|
+
meta: {
|
|
40866
|
+
eventId,
|
|
40367
40867
|
error: String(err)
|
|
40368
40868
|
}
|
|
40369
40869
|
});
|
|
40870
|
+
return false;
|
|
40370
40871
|
}
|
|
40371
40872
|
}
|
|
40873
|
+
/**
|
|
40874
|
+
* The motion-activity LTS tap, exposed for a caller that extends an
|
|
40875
|
+
* already-open episode without inserting a new row (D475) — `insertMotion`
|
|
40876
|
+
* fires it on OPEN; this fires it on every further tick, so a long
|
|
40877
|
+
* continuous episode still samples activity at the same density the
|
|
40878
|
+
* pre-D475 heartbeat gave the aggregator, rather than once per episode.
|
|
40879
|
+
*/
|
|
40880
|
+
noteMotionSample(deviceId, regionCount, at) {
|
|
40881
|
+
this.onMotionSample?.(deviceId, regionCount, at);
|
|
40882
|
+
}
|
|
40883
|
+
/**
|
|
40884
|
+
* Returns the ROWID SQLite assigned, or `null` when the write failed (D474).
|
|
40885
|
+
*
|
|
40886
|
+
* The caller needs it on the very next line — the notification centre reads
|
|
40887
|
+
* it and the marker-still path builds a media key from it — and with an
|
|
40888
|
+
* auto-assigned key the insert is the ONLY place it is knowable. A
|
|
40889
|
+
* placeholder would reach the event bus and the media keys.
|
|
40890
|
+
*/
|
|
40372
40891
|
async insertObject(ev) {
|
|
40373
40892
|
try {
|
|
40374
|
-
const
|
|
40375
|
-
|
|
40893
|
+
const rounded = "bbox" in ev ? {
|
|
40894
|
+
...ev,
|
|
40895
|
+
bbox: roundBbox(ev.bbox)
|
|
40896
|
+
} : ev;
|
|
40897
|
+
const inserted = await this.store.insert.mutate({
|
|
40376
40898
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
40377
|
-
record: {
|
|
40378
|
-
id,
|
|
40379
|
-
data: rest
|
|
40380
|
-
}
|
|
40899
|
+
record: { data: rounded }
|
|
40381
40900
|
});
|
|
40901
|
+
return eventRowId(this.logger, "insertObject", ev.deviceId, inserted.id);
|
|
40382
40902
|
} catch (err) {
|
|
40383
40903
|
this.logger.warn("insertObject failed", {
|
|
40384
40904
|
tags: { deviceId: ev.deviceId },
|
|
40385
40905
|
meta: {
|
|
40386
|
-
|
|
40906
|
+
timestamp: ev.timestamp,
|
|
40387
40907
|
error: String(err)
|
|
40388
40908
|
}
|
|
40389
40909
|
});
|
|
40910
|
+
return null;
|
|
40390
40911
|
}
|
|
40391
40912
|
}
|
|
40913
|
+
/**
|
|
40914
|
+
* Returns the ROWID SQLite assigned, or `null` when the write failed (D474).
|
|
40915
|
+
*
|
|
40916
|
+
* The caller needs it on the very next line — the notification centre reads
|
|
40917
|
+
* it and the marker-still path builds a media key from it — and with an
|
|
40918
|
+
* auto-assigned key the insert is the ONLY place it is knowable. A
|
|
40919
|
+
* placeholder would reach the event bus and the media keys.
|
|
40920
|
+
*/
|
|
40392
40921
|
async insertAudio(ev) {
|
|
40393
40922
|
try {
|
|
40394
|
-
const
|
|
40395
|
-
await this.store.insert.mutate({
|
|
40923
|
+
const inserted = await this.store.insert.mutate({
|
|
40396
40924
|
collection: AUDIO_EVENTS_COLLECTION,
|
|
40397
|
-
record: {
|
|
40398
|
-
id,
|
|
40399
|
-
data: rest
|
|
40400
|
-
}
|
|
40925
|
+
record: { data: ev }
|
|
40401
40926
|
});
|
|
40927
|
+
return eventRowId(this.logger, "insertAudio", ev.deviceId, inserted.id);
|
|
40402
40928
|
} catch (err) {
|
|
40403
40929
|
this.logger.warn("insertAudio failed", {
|
|
40404
40930
|
tags: { deviceId: ev.deviceId },
|
|
40405
40931
|
meta: {
|
|
40406
|
-
|
|
40932
|
+
timestamp: ev.timestamp,
|
|
40407
40933
|
error: String(err)
|
|
40408
40934
|
}
|
|
40409
40935
|
});
|
|
40936
|
+
return null;
|
|
40410
40937
|
}
|
|
40411
40938
|
}
|
|
40412
40939
|
/**
|
|
@@ -40520,12 +41047,15 @@ var EventStore = class {
|
|
|
40520
41047
|
filter: this.buildFilter(q),
|
|
40521
41048
|
...this.slimColumns(q.projection, SLIM_MOTION_COLUMNS)
|
|
40522
41049
|
});
|
|
40523
|
-
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls$1(r.data)));
|
|
41050
|
+
if (q.projection === "slim") return rows.map((r) => slimMotion(Number(r.id), stripNulls$1(r.data)));
|
|
40524
41051
|
return rows.map((r) => {
|
|
41052
|
+
const { edges: rawEdges, durationMs: rawDurationMs, ...data } = stripNulls$1(r.data);
|
|
40525
41053
|
return {
|
|
40526
|
-
id: r.id,
|
|
41054
|
+
id: Number(r.id),
|
|
40527
41055
|
kind: "motion",
|
|
40528
|
-
...
|
|
41056
|
+
...data,
|
|
41057
|
+
...rawDurationMs !== void 0 ? { durationMs: rawDurationMs === OPEN_EPISODE_SENTINEL ? null : rawDurationMs } : {},
|
|
41058
|
+
...rawEdges !== void 0 ? { edges: decodeMotionEdges(rawEdges) } : {}
|
|
40529
41059
|
};
|
|
40530
41060
|
});
|
|
40531
41061
|
}
|
|
@@ -40572,7 +41102,7 @@ var EventStore = class {
|
|
|
40572
41102
|
});
|
|
40573
41103
|
if (rows.length === 0) break;
|
|
40574
41104
|
for (const r of rows) out.push({
|
|
40575
|
-
id: r.id,
|
|
41105
|
+
id: Number(r.id),
|
|
40576
41106
|
kind: "object",
|
|
40577
41107
|
...stripNulls$1(r.data)
|
|
40578
41108
|
});
|
|
@@ -40591,10 +41121,10 @@ var EventStore = class {
|
|
|
40591
41121
|
filter,
|
|
40592
41122
|
...this.slimColumns(q.projection, SLIM_OBJECT_COLUMNS)
|
|
40593
41123
|
});
|
|
40594
|
-
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls$1(r.data)));
|
|
41124
|
+
if (q.projection === "slim") return rows.map((r) => slimObject(Number(r.id), stripNulls$1(r.data)));
|
|
40595
41125
|
return rows.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
|
};
|
|
@@ -40619,7 +41149,7 @@ var EventStore = class {
|
|
|
40619
41149
|
}
|
|
40620
41150
|
})).map((r) => {
|
|
40621
41151
|
return {
|
|
40622
|
-
id: r.id,
|
|
41152
|
+
id: Number(r.id),
|
|
40623
41153
|
kind: "object",
|
|
40624
41154
|
...stripNulls$1(r.data)
|
|
40625
41155
|
};
|
|
@@ -40630,10 +41160,10 @@ var EventStore = class {
|
|
|
40630
41160
|
collection: AUDIO_EVENTS_COLLECTION,
|
|
40631
41161
|
filter: this.buildFilter(q)
|
|
40632
41162
|
});
|
|
40633
|
-
if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls$1(r.data)));
|
|
41163
|
+
if (q.projection === "slim") return rows.map((r) => slimAudio(Number(r.id), stripNulls$1(r.data)));
|
|
40634
41164
|
return rows.map((r) => {
|
|
40635
41165
|
return {
|
|
40636
|
-
id: r.id,
|
|
41166
|
+
id: Number(r.id),
|
|
40637
41167
|
kind: "audio",
|
|
40638
41168
|
...stripNulls$1(r.data)
|
|
40639
41169
|
};
|
|
@@ -40644,9 +41174,28 @@ var EventStore = class {
|
|
|
40644
41174
|
* resolver's track fallback: a new-style object event owns no crop, so its
|
|
40645
41175
|
* `mediaUrl` degrades to the track's cadence media, addressed by this id.
|
|
40646
41176
|
* Returns `null` when the id is not a persisted object event (e.g. a KeyEvent
|
|
40647
|
-
* whose id is already a track id
|
|
40648
|
-
|
|
40649
|
-
|
|
41177
|
+
* whose id is already a track id) — best-effort.
|
|
41178
|
+
*
|
|
41179
|
+
* **D476: only an OBJECT event has a `trackId` at all** — motion and audio
|
|
41180
|
+
* rows carry no such column, so this probes `OBJECT_EVENTS_COLLECTION`
|
|
41181
|
+
* only, never the other two. That was already true structurally, but before
|
|
41182
|
+
* D474 an id that MISSED here was safely "not an object event" because
|
|
41183
|
+
* every id was a UUID, unique across all three tables. D474 made event ids
|
|
41184
|
+
* per-table rowid aliases, so a motion id can now coincide with an
|
|
41185
|
+
* UNRELATED object row of the same number — a probe that HITS is no longer
|
|
41186
|
+
* proof the caller's id names that object row.
|
|
41187
|
+
*
|
|
41188
|
+
* `ownerType`, when the caller already knows it (a `motion:`/`audio:`/
|
|
41189
|
+
* `object:` key was parsed rather than a bare legacy id), short-circuits to
|
|
41190
|
+
* `null` without querying for anything but `'object'` — the caller's one
|
|
41191
|
+
* remaining lever against the collision, since this method has nothing left
|
|
41192
|
+
* to guess once the kind is in the key. Its only caller today
|
|
41193
|
+
* (`readEventThumbnail` in `index.ts`) still reaches this with a bare,
|
|
41194
|
+
* kind-less id from a URL and cannot supply it — see the D476 ADR for why
|
|
41195
|
+
* that residual ambiguity is accepted rather than fixed here.
|
|
41196
|
+
*/
|
|
41197
|
+
async getTrackIdForEvent(eventId, ownerType) {
|
|
41198
|
+
if (ownerType !== void 0 && ownerType !== "object") return null;
|
|
40650
41199
|
try {
|
|
40651
41200
|
const row = await this.store.get.query({
|
|
40652
41201
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
@@ -40999,9 +41548,11 @@ var EventStore = class {
|
|
|
40999
41548
|
});
|
|
41000
41549
|
const motion = await batch(MOTION_EVENTS_COLLECTION);
|
|
41001
41550
|
const audio = await batch(AUDIO_EVENTS_COLLECTION);
|
|
41002
|
-
|
|
41003
|
-
|
|
41004
|
-
|
|
41551
|
+
if ((motion.length > 0 || audio.length > 0) && this.media !== void 0) try {
|
|
41552
|
+
await this.media.deleteForEvents({
|
|
41553
|
+
motion,
|
|
41554
|
+
audio
|
|
41555
|
+
});
|
|
41005
41556
|
} catch (err) {
|
|
41006
41557
|
this.logger.warn("EventStore.evictTracklessBefore: event media delete failed", { meta: { error: String(err) } });
|
|
41007
41558
|
}
|
|
@@ -41067,7 +41618,7 @@ var EventStore = class {
|
|
|
41067
41618
|
* {@link existingEventIds} answers "is this event alive", which is the
|
|
41068
41619
|
* orphan audit's question. The media reclaim asks a narrower one: *is this
|
|
41069
41620
|
* event-owned snapshot a MOTION-VISIT still* — because the selection shape
|
|
41070
|
-
* (`ownerKind: '
|
|
41621
|
+
* (`ownerKind: 'motion', kind: 'snapshot'`) is not itself a guarantee.
|
|
41071
41622
|
* Nothing structurally prevents another writer landing an event-owned
|
|
41072
41623
|
* snapshot on an object or audio row, and the reclaim must not be the place
|
|
41073
41624
|
* that discovers it. A per-page indexed `whereIn` on one table is cheap
|
|
@@ -41134,7 +41685,12 @@ var EventStore = class {
|
|
|
41134
41685
|
...motionIds,
|
|
41135
41686
|
...objectIds,
|
|
41136
41687
|
...audioIds
|
|
41137
|
-
]
|
|
41688
|
+
],
|
|
41689
|
+
byKind: {
|
|
41690
|
+
motion: motionIds,
|
|
41691
|
+
object: objectIds,
|
|
41692
|
+
audio: audioIds
|
|
41693
|
+
}
|
|
41138
41694
|
};
|
|
41139
41695
|
}
|
|
41140
41696
|
/**
|
|
@@ -41199,7 +41755,7 @@ var EventStore = class {
|
|
|
41199
41755
|
}
|
|
41200
41756
|
let mediaRows = 0;
|
|
41201
41757
|
if (eventIds.length > 0 && this.media !== void 0) try {
|
|
41202
|
-
mediaRows = await this.media.deleteForEvents(eventIds);
|
|
41758
|
+
mediaRows = await this.media.deleteForEvents({ object: eventIds });
|
|
41203
41759
|
} catch (err) {
|
|
41204
41760
|
this.logger.warn("EventStore.deleteByTracks: event media delete failed", { meta: { error: String(err) } });
|
|
41205
41761
|
}
|
|
@@ -41529,8 +42085,8 @@ function rowMatchesZone(data, zone) {
|
|
|
41529
42085
|
const fw = data["frameWidth"];
|
|
41530
42086
|
const fh = data["frameHeight"];
|
|
41531
42087
|
if (typeof fw !== "number" || typeof fh !== "number") return true;
|
|
41532
|
-
const positions = data["positions"];
|
|
41533
|
-
if (
|
|
42088
|
+
const positions = decodePositions(data["positions"]);
|
|
42089
|
+
if (positions.length === 0) return true;
|
|
41534
42090
|
const positionRows = [];
|
|
41535
42091
|
for (const p of positions) {
|
|
41536
42092
|
if (p === null || typeof p !== "object") continue;
|
|
@@ -43105,7 +43661,13 @@ var TrackStore = class {
|
|
|
43105
43661
|
data
|
|
43106
43662
|
});
|
|
43107
43663
|
matched.sort((a, b) => Number(b.data["firstSeen"] ?? 0) - Number(a.data["firstSeen"] ?? 0));
|
|
43108
|
-
return matched.slice(0, limit).map((r) =>
|
|
43664
|
+
return matched.slice(0, limit).map((r) => {
|
|
43665
|
+
const track = this.rowToTrack(r.id, r.data, params.projection);
|
|
43666
|
+
return {
|
|
43667
|
+
...track,
|
|
43668
|
+
positions: geometryOnlyPositions(track.positions)
|
|
43669
|
+
};
|
|
43670
|
+
});
|
|
43109
43671
|
}
|
|
43110
43672
|
/**
|
|
43111
43673
|
* Batched multi-device recent-tracks page (`listRecentTracks`): the
|
|
@@ -43360,7 +43922,7 @@ var TrackStore = class {
|
|
|
43360
43922
|
...t.producingDeviceName !== void 0 ? { producingDeviceName: t.producingDeviceName } : {},
|
|
43361
43923
|
firstSeen: t.firstSeen,
|
|
43362
43924
|
lastSeen: t.lastSeen,
|
|
43363
|
-
positions:
|
|
43925
|
+
positions: encodePositions(t.positions),
|
|
43364
43926
|
observations: t.positions.length,
|
|
43365
43927
|
snapshots: [...t.snapshots],
|
|
43366
43928
|
zonesVisited: [...t.zonesVisited],
|
|
@@ -43389,7 +43951,7 @@ var TrackStore = class {
|
|
|
43389
43951
|
...t.source !== void 0 ? { source: t.source } : {},
|
|
43390
43952
|
firstSeen: t.firstSeen,
|
|
43391
43953
|
lastSeen: t.lastSeen,
|
|
43392
|
-
positions:
|
|
43954
|
+
positions: encodePositions(t.positions),
|
|
43393
43955
|
observations: t.positions.length,
|
|
43394
43956
|
snapshots: [...t.snapshots],
|
|
43395
43957
|
zonesVisited: [...t.zonesVisited],
|
|
@@ -43429,7 +43991,7 @@ var TrackStore = class {
|
|
|
43429
43991
|
*/
|
|
43430
43992
|
rowToTrack(id, data, projection) {
|
|
43431
43993
|
const slim = projection === "slim";
|
|
43432
|
-
const storedPositions = slim ? [] : data["positions"]
|
|
43994
|
+
const storedPositions = slim ? [] : decodePositions(data["positions"]);
|
|
43433
43995
|
/**
|
|
43434
43996
|
* The stored count first, the blob only as a fallback — never `0` for a
|
|
43435
43997
|
* column that was not read.
|
|
@@ -44396,7 +44958,7 @@ function createDiscardJob(deps) {
|
|
|
44396
44958
|
collection: TRACKS_COLLECTION,
|
|
44397
44959
|
key: owner.ownerId
|
|
44398
44960
|
});
|
|
44399
|
-
} else if (owner.ownerKind === "
|
|
44961
|
+
} else if (owner.ownerKind === "object") await deps.store.delete.mutate({
|
|
44400
44962
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
44401
44963
|
key: owner.ownerId
|
|
44402
44964
|
});
|
|
@@ -45074,21 +45636,29 @@ function allEventIds(events) {
|
|
|
45074
45636
|
/**
|
|
45075
45637
|
* Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
|
|
45076
45638
|
* `resolveDefaultEventMedia`: the event's OWN media when it has any, else its
|
|
45077
|
-
* owning TRACK's.
|
|
45078
|
-
*
|
|
45079
|
-
*
|
|
45639
|
+
* owning TRACK's. Up to four batched index reads — one PER EVENT KIND that has
|
|
45640
|
+
* any ids (D476: `motion` / `object` / `audio` are separate owner kinds now,
|
|
45641
|
+
* so a single combined read can no longer ask "does any of these ids own
|
|
45642
|
+
* media" — a motion id and an object id can be the same number post-D474 and
|
|
45643
|
+
* are answered by different rows) — plus the track fallback, only over the
|
|
45644
|
+
* object events the first pass did not already answer for (motion and audio
|
|
45645
|
+
* are trackless roots, so they never need it).
|
|
45080
45646
|
*
|
|
45081
45647
|
* THROWS on a failed read. The caller fails closed on the CLAIM and open on the
|
|
45082
45648
|
* LIST; swallowing here would collapse those two opposite directions into one.
|
|
45083
45649
|
*/
|
|
45084
45650
|
async function resolveVouchedEvents(deviceId, events, fetchMediaOwners) {
|
|
45085
|
-
const
|
|
45086
|
-
|
|
45087
|
-
|
|
45088
|
-
|
|
45089
|
-
|
|
45090
|
-
|
|
45091
|
-
|
|
45651
|
+
const ownMedia = /* @__PURE__ */ new Set();
|
|
45652
|
+
for (const ownerType of require_dist.EVENT_OWNER_TYPES) {
|
|
45653
|
+
const ownerIds = events[ownerType].map((e) => e.id);
|
|
45654
|
+
if (ownerIds.length === 0) continue;
|
|
45655
|
+
const found = await fetchMediaOwners({
|
|
45656
|
+
deviceId,
|
|
45657
|
+
ownerKind: ownerType,
|
|
45658
|
+
ownerIds
|
|
45659
|
+
});
|
|
45660
|
+
for (const id of found) ownMedia.add(id);
|
|
45661
|
+
}
|
|
45092
45662
|
const residualTracks = [];
|
|
45093
45663
|
const seenTracks = /* @__PURE__ */ new Set();
|
|
45094
45664
|
for (const event of events.object) {
|
|
@@ -45203,6 +45773,15 @@ function reportUnvouchedClips(deps, deviceId, clips) {
|
|
|
45203
45773
|
});
|
|
45204
45774
|
}
|
|
45205
45775
|
//#endregion
|
|
45776
|
+
//#region src/pipeline-analytics/videoclips-shared.ts
|
|
45777
|
+
/** Stringify the numeric event ids of one bucket. Lossless. */
|
|
45778
|
+
function toEventLike(events) {
|
|
45779
|
+
return events.map((e) => ({
|
|
45780
|
+
...e,
|
|
45781
|
+
id: String(e.id)
|
|
45782
|
+
}));
|
|
45783
|
+
}
|
|
45784
|
+
//#endregion
|
|
45206
45785
|
//#region src/pipeline-analytics/viewer-settings-actions.ts
|
|
45207
45786
|
/**
|
|
45208
45787
|
* Viewer settings snapshots — hub-shared named blobs of the viewer's stores.
|
|
@@ -46258,7 +46837,13 @@ var AnalyticsQueryFacade = class {
|
|
|
46258
46837
|
const store = this.deps.mediaStore();
|
|
46259
46838
|
if (store === null) return [];
|
|
46260
46839
|
const prefix = this.deps.eventMediaPathPrefix();
|
|
46261
|
-
|
|
46840
|
+
const kinds = input.kind === void 0 ? void 0 : [input.kind];
|
|
46841
|
+
let rows = [];
|
|
46842
|
+
for (const ownerType of require_dist.EVENT_OWNER_TYPES) {
|
|
46843
|
+
rows = await store.listByOwner(ownerType, input.eventId, input.deviceId, kinds);
|
|
46844
|
+
if (rows.length > 0) break;
|
|
46845
|
+
}
|
|
46846
|
+
return rows.map((row) => toWireMediaFile(row, prefix));
|
|
46262
46847
|
}
|
|
46263
46848
|
/**
|
|
46264
46849
|
* A track's media WITH the bytes, restricted to `kinds` when given.
|
|
@@ -46300,7 +46885,12 @@ var AnalyticsQueryFacade = class {
|
|
|
46300
46885
|
const store = this.deps.mediaStore();
|
|
46301
46886
|
if (store === null) return [];
|
|
46302
46887
|
const prefix = this.deps.eventMediaPathPrefix();
|
|
46303
|
-
|
|
46888
|
+
let rows = [];
|
|
46889
|
+
for (const ownerType of require_dist.EVENT_OWNER_TYPES) {
|
|
46890
|
+
rows = await store.listInfoByOwner(ownerType, input.eventId, input.deviceId);
|
|
46891
|
+
if (rows.length > 0) break;
|
|
46892
|
+
}
|
|
46893
|
+
return rows.map((row) => toWireMediaFileInfo(row, prefix));
|
|
46304
46894
|
}
|
|
46305
46895
|
/**
|
|
46306
46896
|
* A track's media WITHOUT the bytes.
|
|
@@ -46334,9 +46924,9 @@ var AnalyticsQueryFacade = class {
|
|
|
46334
46924
|
object: 0,
|
|
46335
46925
|
audio: 0
|
|
46336
46926
|
};
|
|
46337
|
-
const { counts, ids } = await eventStore.pruneBefore(input);
|
|
46927
|
+
const { counts, ids, byKind } = await eventStore.pruneBefore(input);
|
|
46338
46928
|
const mediaStore = this.deps.mediaStore();
|
|
46339
|
-
if (ids.length > 0 && mediaStore) await mediaStore.deleteForEvents(
|
|
46929
|
+
if (ids.length > 0 && mediaStore) await mediaStore.deleteForEvents(byKind);
|
|
46340
46930
|
const { motion, object, audio } = counts;
|
|
46341
46931
|
if (motion + object + audio > 0) this.deps.logger().info("analytics event eviction (floor)", {
|
|
46342
46932
|
tags: { deviceId: input.deviceId },
|
|
@@ -46458,7 +47048,11 @@ var AnalyticsQueryFacade = class {
|
|
|
46458
47048
|
...evicted.audio
|
|
46459
47049
|
];
|
|
46460
47050
|
const mediaStore = this.deps.mediaStore();
|
|
46461
|
-
if (ids.length > 0 && mediaStore) await mediaStore.deleteForEvents(
|
|
47051
|
+
if (ids.length > 0 && mediaStore) await mediaStore.deleteForEvents({
|
|
47052
|
+
motion: evicted.motion,
|
|
47053
|
+
object: objectIds,
|
|
47054
|
+
audio: evicted.audio
|
|
47055
|
+
});
|
|
46462
47056
|
const counts = {
|
|
46463
47057
|
motion: evicted.motion.length,
|
|
46464
47058
|
object: objectIds.length,
|
|
@@ -49297,9 +49891,13 @@ function resolveCropForcedMedia(files, pickClean, detailFiles = []) {
|
|
|
49297
49891
|
* Event + track media (the body/scene) and face/plate media (the detail
|
|
49298
49892
|
* crops). Face rows live under `face-${trackId}`; a track can close with
|
|
49299
49893
|
* `hasEmbeddedFace` and ZERO track files (0e343f86).
|
|
49894
|
+
*
|
|
49895
|
+
* `eventFiles` is the event's OWN media, already fetched by the caller (the
|
|
49896
|
+
* same rows the default path reuses via {@link collectDefaultEventMedia}) —
|
|
49897
|
+
* this function no longer lists by owner kind `'event'` itself (D476; see
|
|
49898
|
+
* {@link CropForcedOwnerKind}).
|
|
49300
49899
|
*/
|
|
49301
|
-
async function collectCropForcedMedia(source, id) {
|
|
49302
|
-
const eventFiles = await source.listByOwner("event", id);
|
|
49900
|
+
async function collectCropForcedMedia(source, id, eventFiles) {
|
|
49303
49901
|
const trackFiles = await source.listByOwner("track", id);
|
|
49304
49902
|
const ownerTrackId = trackFiles.length === 0 ? await source.getTrackIdForEvent(id) : null;
|
|
49305
49903
|
const owningTrackFiles = ownerTrackId === null ? [] : await source.listByOwner("track", ownerTrackId);
|
|
@@ -56253,11 +56851,23 @@ var PACKAGE_IMPORTANCE = 1;
|
|
|
56253
56851
|
* stay `getObjectEvents`-output-valid. */
|
|
56254
56852
|
var DELIVERED_STATE = "idle";
|
|
56255
56853
|
var PICKED_UP_STATE = "left";
|
|
56256
|
-
/**
|
|
56257
|
-
|
|
56854
|
+
/**
|
|
56855
|
+
* Deterministic IDEMPOTENCE keys, one per (stationary entry, phase).
|
|
56856
|
+
*
|
|
56857
|
+
* These were the event ids until D474 made an event id the SQLite rowid, which
|
|
56858
|
+
* the database assigns and a producer cannot choose. They kept their exact
|
|
56859
|
+
* spelling and moved to `ObjectEvent.idempotencyKey`: the gates below are the
|
|
56860
|
+
* only reason a restart does not re-deliver every parcel, and the strings are
|
|
56861
|
+
* on 2.1 million existing rows' primary key, so a row written before the
|
|
56862
|
+
* rebuild is still recognisable by the same text.
|
|
56863
|
+
*
|
|
56864
|
+
* They are NOT event ids any more. `eventId` on the emitted payload and in
|
|
56865
|
+
* every log line is the row's real id.
|
|
56866
|
+
*/
|
|
56867
|
+
function deliveredIdempotencyKey(entryId) {
|
|
56258
56868
|
return `pa-pkg-${entryId}-delivered`;
|
|
56259
56869
|
}
|
|
56260
|
-
function
|
|
56870
|
+
function pickedUpIdempotencyKey(entryId) {
|
|
56261
56871
|
return `pa-pkg-${entryId}-pickedup`;
|
|
56262
56872
|
}
|
|
56263
56873
|
var PackageDropDetector = class {
|
|
@@ -56395,26 +57005,28 @@ var PackageDropDetector = class {
|
|
|
56395
57005
|
});
|
|
56396
57006
|
return;
|
|
56397
57007
|
}
|
|
56398
|
-
const
|
|
56399
|
-
|
|
57008
|
+
const idempotencyKey = deliveredIdempotencyKey(entry.id);
|
|
57009
|
+
const alreadyDelivered = (await this.deps.events.queryObject({
|
|
56400
57010
|
deviceId: entry.deviceId,
|
|
56401
|
-
classFilter:
|
|
56402
|
-
})).
|
|
57011
|
+
classFilter: PACKAGE_EVENT_CLASS
|
|
57012
|
+
})).find((e) => e.idempotencyKey === idempotencyKey || String(e.id) === idempotencyKey);
|
|
57013
|
+
if (alreadyDelivered !== void 0) {
|
|
56403
57014
|
this.deps.logger.info("package gate 5/5 idempotence — delivery already recorded", {
|
|
56404
57015
|
tags,
|
|
56405
57016
|
meta: {
|
|
56406
57017
|
entryId: entry.id,
|
|
56407
|
-
|
|
57018
|
+
idempotencyKey,
|
|
57019
|
+
eventId: alreadyDelivered.id
|
|
56408
57020
|
}
|
|
56409
57021
|
});
|
|
56410
57022
|
return;
|
|
56411
57023
|
}
|
|
56412
|
-
const
|
|
56413
|
-
id: eventId,
|
|
57024
|
+
const draft = {
|
|
56414
57025
|
kind: "object",
|
|
56415
57026
|
deviceId: entry.deviceId,
|
|
56416
57027
|
timestamp,
|
|
56417
57028
|
source: "pipeline",
|
|
57029
|
+
idempotencyKey,
|
|
56418
57030
|
trackId: entry.sourceTrackId ?? entry.id,
|
|
56419
57031
|
className: PACKAGE_EVENT_CLASS,
|
|
56420
57032
|
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
@@ -56432,12 +57044,26 @@ var PackageDropDetector = class {
|
|
|
56432
57044
|
...entry.keyFrameMediaKey !== void 0 ? { mediaKey: entry.keyFrameMediaKey } : {},
|
|
56433
57045
|
importance: PACKAGE_IMPORTANCE
|
|
56434
57046
|
};
|
|
56435
|
-
await this.deps.events.insertObject(
|
|
57047
|
+
const rowId = await this.deps.events.insertObject(draft);
|
|
57048
|
+
if (rowId === null) {
|
|
57049
|
+
this.deps.logger.warn("package delivery NOT emitted — the event row did not persist", {
|
|
57050
|
+
tags,
|
|
57051
|
+
meta: {
|
|
57052
|
+
entryId: entry.id,
|
|
57053
|
+
idempotencyKey
|
|
57054
|
+
}
|
|
57055
|
+
});
|
|
57056
|
+
return;
|
|
57057
|
+
}
|
|
57058
|
+
const ev = {
|
|
57059
|
+
...draft,
|
|
57060
|
+
id: rowId
|
|
57061
|
+
};
|
|
56436
57062
|
this.deps.onPersisted?.(ev, "delivered");
|
|
56437
57063
|
this.deps.emit.delivered({
|
|
56438
57064
|
deviceId: entry.deviceId,
|
|
56439
57065
|
entryId: entry.id,
|
|
56440
|
-
eventId,
|
|
57066
|
+
eventId: String(rowId),
|
|
56441
57067
|
className: entry.className,
|
|
56442
57068
|
zoneIds: hitZones,
|
|
56443
57069
|
...entry.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: entry.keyFrameMediaKey } : {},
|
|
@@ -56453,7 +57079,8 @@ var PackageDropDetector = class {
|
|
|
56453
57079
|
tags: { deviceId: entry.deviceId },
|
|
56454
57080
|
meta: {
|
|
56455
57081
|
entryId: entry.id,
|
|
56456
|
-
eventId,
|
|
57082
|
+
eventId: rowId,
|
|
57083
|
+
idempotencyKey,
|
|
56457
57084
|
zoneIds: hitZones,
|
|
56458
57085
|
dwellMs,
|
|
56459
57086
|
areaFrac: Number(areaFrac.toFixed(5)),
|
|
@@ -56471,13 +57098,14 @@ var PackageDropDetector = class {
|
|
|
56471
57098
|
});
|
|
56472
57099
|
return;
|
|
56473
57100
|
}
|
|
56474
|
-
const
|
|
56475
|
-
const
|
|
57101
|
+
const deliveredKey = deliveredIdempotencyKey(entry.id);
|
|
57102
|
+
const pickedUpKey = pickedUpIdempotencyKey(entry.id);
|
|
56476
57103
|
const rows = await this.deps.events.queryObject({
|
|
56477
57104
|
deviceId: entry.deviceId,
|
|
56478
57105
|
classFilter: PACKAGE_EVENT_CLASS
|
|
56479
57106
|
});
|
|
56480
|
-
const
|
|
57107
|
+
const matchesKey = (e, key) => e.idempotencyKey === key || String(e.id) === key;
|
|
57108
|
+
const delivered = rows.find((e) => matchesKey(e, deliveredKey));
|
|
56481
57109
|
if (delivered === void 0) {
|
|
56482
57110
|
this.deps.logger.debug("package pick-up skipped — no delivery row for this entry", {
|
|
56483
57111
|
tags: { deviceId: entry.deviceId },
|
|
@@ -56488,12 +57116,14 @@ var PackageDropDetector = class {
|
|
|
56488
57116
|
});
|
|
56489
57117
|
return;
|
|
56490
57118
|
}
|
|
56491
|
-
|
|
57119
|
+
const alreadyPickedUp = rows.find((e) => matchesKey(e, pickedUpKey));
|
|
57120
|
+
if (alreadyPickedUp !== void 0) {
|
|
56492
57121
|
this.deps.logger.info("package pick-up skipped — already recorded", {
|
|
56493
57122
|
tags: { deviceId: entry.deviceId },
|
|
56494
57123
|
meta: {
|
|
56495
57124
|
entryId: entry.id,
|
|
56496
|
-
eventId:
|
|
57125
|
+
eventId: alreadyPickedUp.id,
|
|
57126
|
+
idempotencyKey: pickedUpKey
|
|
56497
57127
|
}
|
|
56498
57128
|
});
|
|
56499
57129
|
return;
|
|
@@ -56505,17 +57135,17 @@ var PackageDropDetector = class {
|
|
|
56505
57135
|
tags: { deviceId: entry.deviceId },
|
|
56506
57136
|
meta: {
|
|
56507
57137
|
entryId: entry.id,
|
|
56508
|
-
|
|
57138
|
+
idempotencyKey: pickedUpKey,
|
|
56509
57139
|
packageTrackId,
|
|
56510
57140
|
pickupAt: entry.lastConfirmedAt
|
|
56511
57141
|
}
|
|
56512
57142
|
});
|
|
56513
|
-
const
|
|
56514
|
-
id: pickedUpId,
|
|
57143
|
+
const draft = {
|
|
56515
57144
|
kind: "object",
|
|
56516
57145
|
deviceId: entry.deviceId,
|
|
56517
57146
|
timestamp,
|
|
56518
57147
|
source: "pipeline",
|
|
57148
|
+
idempotencyKey: pickedUpKey,
|
|
56519
57149
|
trackId: mediaTrackId,
|
|
56520
57150
|
className: PACKAGE_EVENT_CLASS,
|
|
56521
57151
|
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
@@ -56533,12 +57163,26 @@ var PackageDropDetector = class {
|
|
|
56533
57163
|
...collector?.keyFrameMediaKey !== void 0 ? { mediaKey: collector.keyFrameMediaKey } : {},
|
|
56534
57164
|
importance: PACKAGE_IMPORTANCE
|
|
56535
57165
|
};
|
|
56536
|
-
await this.deps.events.insertObject(
|
|
57166
|
+
const rowId = await this.deps.events.insertObject(draft);
|
|
57167
|
+
if (rowId === null) {
|
|
57168
|
+
this.deps.logger.warn("package pick-up NOT emitted — the event row did not persist", {
|
|
57169
|
+
tags: { deviceId: entry.deviceId },
|
|
57170
|
+
meta: {
|
|
57171
|
+
entryId: entry.id,
|
|
57172
|
+
idempotencyKey: pickedUpKey
|
|
57173
|
+
}
|
|
57174
|
+
});
|
|
57175
|
+
return;
|
|
57176
|
+
}
|
|
57177
|
+
const ev = {
|
|
57178
|
+
...draft,
|
|
57179
|
+
id: rowId
|
|
57180
|
+
};
|
|
56537
57181
|
this.deps.onPersisted?.(ev, "picked-up");
|
|
56538
57182
|
this.deps.emit.pickedUp({
|
|
56539
57183
|
deviceId: entry.deviceId,
|
|
56540
57184
|
entryId: entry.id,
|
|
56541
|
-
deliveredEventId:
|
|
57185
|
+
deliveredEventId: String(delivered.id),
|
|
56542
57186
|
className: entry.className,
|
|
56543
57187
|
...collector !== void 0 ? { collectorTrackId: collector.trackId } : {},
|
|
56544
57188
|
timestamp
|
|
@@ -56547,8 +57191,9 @@ var PackageDropDetector = class {
|
|
|
56547
57191
|
tags: { deviceId: entry.deviceId },
|
|
56548
57192
|
meta: {
|
|
56549
57193
|
entryId: entry.id,
|
|
56550
|
-
eventId:
|
|
56551
|
-
|
|
57194
|
+
eventId: rowId,
|
|
57195
|
+
idempotencyKey: pickedUpKey,
|
|
57196
|
+
deliveredEventId: delivered.id,
|
|
56552
57197
|
mediaTrackId,
|
|
56553
57198
|
...collector !== void 0 ? {
|
|
56554
57199
|
collectorTrackId: collector.trackId,
|
|
@@ -58983,7 +59628,7 @@ async function loadRebuildSource(deps, trackId) {
|
|
|
58983
59628
|
const cropInfo = media.find((m) => m.kind === "thumbnail");
|
|
58984
59629
|
const peak = await deps.events.peakForTrack(trackId);
|
|
58985
59630
|
const events = await deps.events.queryObjectByTrackIds([trackId]);
|
|
58986
|
-
const best = events.find((e) => e.id === peak.bestEventId) ?? events[0];
|
|
59631
|
+
const best = events.find((e) => String(e.id) === peak.bestEventId) ?? events[0];
|
|
58987
59632
|
if (best === void 0) return null;
|
|
58988
59633
|
const common = {
|
|
58989
59634
|
trackId,
|
|
@@ -62876,7 +63521,7 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
62876
63521
|
classification: {
|
|
62877
63522
|
kind: "owner-indirection",
|
|
62878
63523
|
derivation: "explicit",
|
|
62879
|
-
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."
|
|
63524
|
+
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."
|
|
62880
63525
|
}
|
|
62881
63526
|
},
|
|
62882
63527
|
{
|
|
@@ -63090,7 +63735,9 @@ function resolveMediaOwner(row) {
|
|
|
63090
63735
|
kind: "track",
|
|
63091
63736
|
trackId: ownerId
|
|
63092
63737
|
};
|
|
63093
|
-
case "
|
|
63738
|
+
case "motion":
|
|
63739
|
+
case "object":
|
|
63740
|
+
case "audio": return {
|
|
63094
63741
|
kind: "event",
|
|
63095
63742
|
eventId: ownerId
|
|
63096
63743
|
};
|
|
@@ -64359,8 +65006,21 @@ var FULL_FRAME_BBOX = {
|
|
|
64359
65006
|
w: 1,
|
|
64360
65007
|
h: 1
|
|
64361
65008
|
};
|
|
65009
|
+
/** The same classification the RUNNER applies to decide "full frame" from the
|
|
65010
|
+
* bbox (`getNativeCrop`), restated here so the two cannot drift. */
|
|
65011
|
+
function isFullFrameBbox(bbox) {
|
|
65012
|
+
return bbox.x <= .001 && bbox.y <= .001 && bbox.w >= .999 && bbox.h >= .999;
|
|
65013
|
+
}
|
|
64362
65014
|
/** Throttle for the native-crop HIT/FALLBACK metric window line. */
|
|
64363
65015
|
var NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
|
|
65016
|
+
/**
|
|
65017
|
+
* Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (the
|
|
65018
|
+
* `keyFrame`/group-shot rung, ~1–1.5 MB on a 4K camera) rather than a boxed
|
|
65019
|
+
* timeline tile or a pinned ≤640 detection frame (tens of kB). The two belong
|
|
65020
|
+
* in different counters or the window line says "full frames" and means two
|
|
65021
|
+
* payload classes three orders of magnitude apart. (D470)
|
|
65022
|
+
*/
|
|
65023
|
+
var FULL_FRAME_TILE_WIDTH_CEILING = 1920;
|
|
64364
65024
|
function createNativeFrameTransport(deps) {
|
|
64365
65025
|
const { api, logger } = deps;
|
|
64366
65026
|
const pipelineRunnerApi = api.pipelineRunner;
|
|
@@ -64434,16 +65094,35 @@ function createNativeFrameTransport(deps) {
|
|
|
64434
65094
|
tier: reply.tier === "ram-fullframe" ? "ram-fullframe" : "native"
|
|
64435
65095
|
};
|
|
64436
65096
|
};
|
|
64437
|
-
const getRemoteFrame = async (handle) => requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame);
|
|
64438
|
-
const fetchNativeFullFrameTiered = async (handle, maxWidth) => requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame);
|
|
65097
|
+
const getRemoteFrame = async (handle) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame));
|
|
65098
|
+
const fetchNativeFullFrameTiered = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame));
|
|
64439
65099
|
const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
|
|
64440
65100
|
const cropMetricLogger = logger.child("NativeCrop");
|
|
64441
65101
|
let nativeHits = 0;
|
|
64442
65102
|
let nativeFallbacks = 0;
|
|
64443
65103
|
let lastCropMetricAt = 0;
|
|
65104
|
+
let roiFetches = 0;
|
|
65105
|
+
let fullFrames = 0;
|
|
65106
|
+
let nativeFullFrames = 0;
|
|
64444
65107
|
const bumpCropMetric = (hit) => {
|
|
64445
65108
|
if (hit) nativeHits += 1;
|
|
64446
65109
|
else nativeFallbacks += 1;
|
|
65110
|
+
maybeLogCropWindow();
|
|
65111
|
+
};
|
|
65112
|
+
/**
|
|
65113
|
+
* Publish the window once `request` has SETTLED — never before, or the line
|
|
65114
|
+
* carries a window whose own outcome has not been counted yet and throttles
|
|
65115
|
+
* away the one that has. `finally`, so the deliberate throw of
|
|
65116
|
+
* `fetchNativeFullFrameTiered` still reaches `persistTrackKeyFrame`.
|
|
65117
|
+
*/
|
|
65118
|
+
const closeWindowAfter = async (request) => {
|
|
65119
|
+
try {
|
|
65120
|
+
return await request;
|
|
65121
|
+
} finally {
|
|
65122
|
+
maybeLogCropWindow();
|
|
65123
|
+
}
|
|
65124
|
+
};
|
|
65125
|
+
const maybeLogCropWindow = () => {
|
|
64447
65126
|
const now = Date.now();
|
|
64448
65127
|
if (now - lastCropMetricAt < NATIVE_CROP_METRIC_INTERVAL_MS) return;
|
|
64449
65128
|
lastCropMetricAt = now;
|
|
@@ -64453,11 +65132,20 @@ function createNativeFrameTransport(deps) {
|
|
|
64453
65132
|
detectionFrameFallbacks: nativeFallbacks,
|
|
64454
65133
|
wireFetches: dedupe.fetched,
|
|
64455
65134
|
coalesced: dedupe.coalesced,
|
|
64456
|
-
reusedInWindow: dedupe.reused
|
|
65135
|
+
reusedInWindow: dedupe.reused,
|
|
65136
|
+
roiCalls: roiFetches,
|
|
65137
|
+
tileFullFrameCalls: fullFrames,
|
|
65138
|
+
nativeFullFrameCalls: nativeFullFrames
|
|
64457
65139
|
} });
|
|
65140
|
+
roiFetches = 0;
|
|
65141
|
+
fullFrames = 0;
|
|
65142
|
+
nativeFullFrames = 0;
|
|
64458
65143
|
};
|
|
64459
65144
|
const requestNativeCrop = async (frameHandle, bbox, maxWidth, normalize) => {
|
|
64460
65145
|
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
65146
|
+
if (isFullFrameBbox(bbox)) if (maxWidth !== void 0 && maxWidth > FULL_FRAME_TILE_WIDTH_CEILING) nativeFullFrames += 1;
|
|
65147
|
+
else fullFrames += 1;
|
|
65148
|
+
else roiFetches += 1;
|
|
64461
65149
|
const native = await pipelineRunnerApi.getNativeCrop.query({
|
|
64462
65150
|
handle: frameHandle,
|
|
64463
65151
|
bbox,
|
|
@@ -66506,7 +67194,7 @@ var EventMediaDispatcher = class {
|
|
|
66506
67194
|
if (!childCropData) continue;
|
|
66507
67195
|
await this.deps.mediaStore.put({
|
|
66508
67196
|
deviceId,
|
|
66509
|
-
ownerKind: "
|
|
67197
|
+
ownerKind: "object",
|
|
66510
67198
|
ownerId: ev.eventId,
|
|
66511
67199
|
kind: child.kind,
|
|
66512
67200
|
timestamp: ev.timestamp,
|
|
@@ -66670,7 +67358,7 @@ var MotionEventSnapshotCapturer = class {
|
|
|
66670
67358
|
try {
|
|
66671
67359
|
await this.deps.media.put({
|
|
66672
67360
|
deviceId: input.deviceId,
|
|
66673
|
-
ownerKind: "
|
|
67361
|
+
ownerKind: "motion",
|
|
66674
67362
|
ownerId: input.eventId,
|
|
66675
67363
|
kind: "snapshot",
|
|
66676
67364
|
timestamp: input.timestamp,
|
|
@@ -69643,48 +70331,111 @@ function compareSummaryScores(a, b) {
|
|
|
69643
70331
|
if (a.visible !== b.visible) return a.visible - b.visible;
|
|
69644
70332
|
return a.quality - b.quality;
|
|
69645
70333
|
}
|
|
69646
|
-
/** A candidate replaces the best only when it is STRICTLY better — a tie keeps
|
|
69647
|
-
* the pixels already materialised (they cost a native round trip). */
|
|
69648
|
-
function isBetterSummaryScore(candidate, best) {
|
|
69649
|
-
if (candidate.visible === 0) return false;
|
|
69650
|
-
return best === null || compareSummaryScores(candidate, best) > 0;
|
|
69651
|
-
}
|
|
69652
|
-
//#endregion
|
|
69653
|
-
//#region src/pipeline-analytics/summary/group-shot.ts
|
|
69654
|
-
/**
|
|
69655
|
-
* The running argmax of one open summary's group shot.
|
|
69656
|
-
*
|
|
69657
|
-
* RAM only, on purpose: it decides WHEN pixels are worth a native round trip
|
|
69658
|
-
* (only when the maximum improves — D9/D18), it is not the record of the
|
|
69659
|
-
* shot. The record is the media row the caller writes under the summary's
|
|
69660
|
-
* id; after a restart an open summary simply starts its argmax again and
|
|
69661
|
-
* replaces the row the next time it beats what it can remember, which is
|
|
69662
|
-
* nothing. That is a weaker shot for one session, never a wrong one.
|
|
69663
|
-
*/
|
|
69664
70334
|
var GroupShotTracker = class {
|
|
69665
|
-
|
|
70335
|
+
states = /* @__PURE__ */ new Map();
|
|
70336
|
+
offered = 0;
|
|
70337
|
+
materialised = 0;
|
|
70338
|
+
byGap = 0;
|
|
70339
|
+
byMargin = 0;
|
|
70340
|
+
byWorse = 0;
|
|
69666
70341
|
/**
|
|
69667
|
-
* Offer a frame. Returns `true` when it
|
|
69668
|
-
*
|
|
69669
|
-
*
|
|
70342
|
+
* Offer a frame. Returns `true` when it is worth materialising NOW — the
|
|
70343
|
+
* caller's cue to fetch the pixels for THIS frame, while its handle is
|
|
70344
|
+
* still leased.
|
|
70345
|
+
*
|
|
70346
|
+
* The clock is `frame.timestamp`; the caller injects no second one.
|
|
69670
70347
|
*/
|
|
69671
70348
|
offer(summaryId, score, frame) {
|
|
69672
|
-
if (
|
|
69673
|
-
this.
|
|
70349
|
+
if (score.visible === 0) return false;
|
|
70350
|
+
this.offered += 1;
|
|
70351
|
+
const state = this.states.get(summaryId);
|
|
70352
|
+
if (state === void 0) {
|
|
70353
|
+
this.states.set(summaryId, {
|
|
70354
|
+
delivered: null,
|
|
70355
|
+
lastAttemptAt: frame.timestamp,
|
|
70356
|
+
unlanded: 1
|
|
70357
|
+
});
|
|
70358
|
+
this.materialised += 1;
|
|
70359
|
+
return true;
|
|
70360
|
+
}
|
|
70361
|
+
if (state.lastAttemptAt !== void 0 && frame.timestamp - state.lastAttemptAt < 2e3) {
|
|
70362
|
+
this.byGap += 1;
|
|
70363
|
+
return false;
|
|
70364
|
+
}
|
|
70365
|
+
if (!this.isWorthPixels(state, score)) return false;
|
|
70366
|
+
state.lastAttemptAt = frame.timestamp;
|
|
70367
|
+
state.unlanded += 1;
|
|
70368
|
+
this.materialised += 1;
|
|
70369
|
+
return true;
|
|
70370
|
+
}
|
|
70371
|
+
/**
|
|
70372
|
+
* Nothing has landed yet → keep trying (D58) until the budget is spent.
|
|
70373
|
+
* Something has landed → beat it, by the margin on the quality tiebreak.
|
|
70374
|
+
*/
|
|
70375
|
+
isWorthPixels(state, score) {
|
|
70376
|
+
const delivered = state.delivered;
|
|
70377
|
+
if (delivered === null) {
|
|
70378
|
+
if (state.unlanded >= 4) {
|
|
70379
|
+
this.byMargin += 1;
|
|
70380
|
+
return false;
|
|
70381
|
+
}
|
|
70382
|
+
return true;
|
|
70383
|
+
}
|
|
70384
|
+
if (compareSummaryScores(score, delivered.score) <= 0) {
|
|
70385
|
+
this.byWorse += 1;
|
|
70386
|
+
return false;
|
|
70387
|
+
}
|
|
70388
|
+
if (score.visible === delivered.score.visible && score.quality - delivered.score.quality < .05) {
|
|
70389
|
+
this.byMargin += 1;
|
|
70390
|
+
return false;
|
|
70391
|
+
}
|
|
70392
|
+
return true;
|
|
70393
|
+
}
|
|
70394
|
+
/**
|
|
70395
|
+
* A materialisation LANDED. Only this advances the baseline the next
|
|
70396
|
+
* candidate must beat — a fetch that missed leaves the summary exactly as
|
|
70397
|
+
* picture-less as it was, and the next frame may be worse and still worth
|
|
70398
|
+
* taking.
|
|
70399
|
+
*/
|
|
70400
|
+
noteLanded(summaryId, score, frame) {
|
|
70401
|
+
const state = this.states.get(summaryId);
|
|
70402
|
+
if (state === void 0) return;
|
|
70403
|
+
state.delivered = {
|
|
69674
70404
|
score,
|
|
69675
70405
|
frame
|
|
69676
|
-
}
|
|
69677
|
-
|
|
70406
|
+
};
|
|
70407
|
+
state.unlanded = 0;
|
|
69678
70408
|
}
|
|
69679
|
-
|
|
69680
|
-
|
|
70409
|
+
/** The shot on disk for this summary, as this process last saw it land. */
|
|
70410
|
+
deliveredOf(summaryId) {
|
|
70411
|
+
return this.states.get(summaryId)?.delivered ?? null;
|
|
69681
70412
|
}
|
|
69682
70413
|
/** A sealed or evicted summary takes no more frames; drop its state. */
|
|
69683
70414
|
forget(summaryId) {
|
|
69684
|
-
this.
|
|
70415
|
+
this.states.delete(summaryId);
|
|
70416
|
+
}
|
|
70417
|
+
/**
|
|
70418
|
+
* What the gate did since the last read. Shipped WITH the gate: without it
|
|
70419
|
+
* the only evidence for a fetch that did NOT happen is hub-main's byte
|
|
70420
|
+
* counter, which answers a different question a minute later.
|
|
70421
|
+
*/
|
|
70422
|
+
drainCounts() {
|
|
70423
|
+
const counts = {
|
|
70424
|
+
offered: this.offered,
|
|
70425
|
+
materialised: this.materialised,
|
|
70426
|
+
byGap: this.byGap,
|
|
70427
|
+
byMargin: this.byMargin,
|
|
70428
|
+
byWorse: this.byWorse
|
|
70429
|
+
};
|
|
70430
|
+
this.offered = 0;
|
|
70431
|
+
this.materialised = 0;
|
|
70432
|
+
this.byGap = 0;
|
|
70433
|
+
this.byMargin = 0;
|
|
70434
|
+
this.byWorse = 0;
|
|
70435
|
+
return counts;
|
|
69685
70436
|
}
|
|
69686
70437
|
get size() {
|
|
69687
|
-
return this.
|
|
70438
|
+
return this.states.size;
|
|
69688
70439
|
}
|
|
69689
70440
|
};
|
|
69690
70441
|
//#endregion
|
|
@@ -69981,7 +70732,7 @@ async function buildTrainingExportPlan(readers, input) {
|
|
|
69981
70732
|
scope: "plate"
|
|
69982
70733
|
},
|
|
69983
70734
|
...(eventsByTrack.get(track.trackId) ?? []).map((eventId) => ({
|
|
69984
|
-
ownerKind: "
|
|
70735
|
+
ownerKind: "object",
|
|
69985
70736
|
ownerId: eventId,
|
|
69986
70737
|
scope: `events/${eventId}`
|
|
69987
70738
|
}))
|
|
@@ -70823,6 +71574,8 @@ var RETRAIN_EXPORT_MAX = 2e4;
|
|
|
70823
71574
|
* surface to turn the refinement pipeline on/off for a camera.
|
|
70824
71575
|
*/
|
|
70825
71576
|
var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
71577
|
+
/** Throttle for the group-shot gate's window line (D470). */
|
|
71578
|
+
var GROUP_SHOT_METRIC_INTERVAL_MS = 3e4;
|
|
70826
71579
|
/**
|
|
70827
71580
|
* How often the addon asks "is a gallery sample outside the cluster face
|
|
70828
71581
|
* space?". A flip is rare and the check is two cheap reads — a minute keeps
|
|
@@ -70950,7 +71703,6 @@ var AUDIO_EPISODE_PERSIST_MS = 2e3;
|
|
|
70950
71703
|
* the persistence (D62).
|
|
70951
71704
|
*/
|
|
70952
71705
|
var AUDIO_EVENT_HEARTBEAT_MS = 5e3;
|
|
70953
|
-
var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
70954
71706
|
/**
|
|
70955
71707
|
* Stored media kinds that carry NO drawn bounding box, in fallback preference
|
|
70956
71708
|
* order. The reel forces `?kind=crop`; when a track has no crop the endpoint may
|
|
@@ -71079,8 +71831,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71079
71831
|
summaryStore = null;
|
|
71080
71832
|
/** The ONLY writer of a summary's lifecycle (D359). */
|
|
71081
71833
|
summarySealer = null;
|
|
71082
|
-
/** The
|
|
71834
|
+
/** The materialisation gate on every open summary's group shot (D359 phase 3, D470). RAM only. */
|
|
71083
71835
|
groupShots = new GroupShotTracker();
|
|
71836
|
+
/** Throttle for the group-shot gate's window line (D470). */
|
|
71837
|
+
lastGroupShotMetricAt = 0;
|
|
71084
71838
|
/**
|
|
71085
71839
|
* Tracks a gallery match has named, for the group-shot score's recognition
|
|
71086
71840
|
* term. "Recognised so far" rather than "at that instant": the match arrives
|
|
@@ -71517,7 +72271,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71517
72271
|
secondarySubjects = new SecondarySubjectQueue();
|
|
71518
72272
|
lastFrameDimsByDevice = /* @__PURE__ */ new Map();
|
|
71519
72273
|
lastAudioInsertByDevice = /* @__PURE__ */ new Map();
|
|
71520
|
-
|
|
72274
|
+
lastMotionTickByDevice = /* @__PURE__ */ new Map();
|
|
72275
|
+
motionEpisodes = null;
|
|
71521
72276
|
levelStateByDevice = /* @__PURE__ */ new Map();
|
|
71522
72277
|
/** Per-device corroboration window for classified audio (see
|
|
71523
72278
|
* `audio-confirm-window.ts`). Torn down wherever `levelStateByDevice` is —
|
|
@@ -72686,6 +73441,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
72686
73441
|
media: this.mediaStore
|
|
72687
73442
|
});
|
|
72688
73443
|
this.eventStore = eventStore;
|
|
73444
|
+
this.motionEpisodes = new MotionEpisodeTracker({
|
|
73445
|
+
closeAfterMs: require_dist.MOTION_CLOSE_AFTER_MS,
|
|
73446
|
+
logger: logger.child("MotionEpisodeTracker"),
|
|
73447
|
+
insertRow: (input) => this.insertMotionEpisodeRow(input.deviceId, input.timestamp, input.regionData),
|
|
73448
|
+
closeRow: (input) => this.closeMotionEpisodeRow(input),
|
|
73449
|
+
onNewEdge: (input) => this.onMotionEpisodeEdge(input),
|
|
73450
|
+
onTick: (input) => this.onMotionEpisodeTick(input)
|
|
73451
|
+
});
|
|
72689
73452
|
this.sensorEventStore = new SensorEventStore({
|
|
72690
73453
|
store: api.settingsStore,
|
|
72691
73454
|
logger: logger.child("SensorEventStore")
|
|
@@ -73344,11 +74107,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73344
74107
|
* The mosaic is filed through the SAME `mediaStore` every event thumbnail
|
|
73345
74108
|
* goes through — the operator's directive, verbatim: same data structure,
|
|
73346
74109
|
* same storage location, no second retained store. It is written under
|
|
73347
|
-
* `ownerKind: '
|
|
73348
|
-
*
|
|
73349
|
-
*
|
|
73350
|
-
*
|
|
73351
|
-
*
|
|
74110
|
+
* `ownerKind: 'object'` with the window's own SYNTHETIC owner id (never a
|
|
74111
|
+
* real object-event id), so it lands beside the cameras' event media
|
|
74112
|
+
* (`<deviceId>/events/object/<ownerId>/…`) and is swept by the same
|
|
74113
|
+
* ownership-based orphan audit: `resolveMediaOwner` maps any of
|
|
74114
|
+
* `motion`/`object`/`audio` to the same generic `{kind:'event'}` OwnerRef,
|
|
74115
|
+
* `EventStore.existingEventIds` never finds the synthetic id in any of the
|
|
74116
|
+
* three tables, and the row is reclaimed as an orphan once past the grace
|
|
74117
|
+
* window — same effective lifecycle as before D476, when `event` covered
|
|
74118
|
+
* all three. `object` is a pragmatic choice among equals here, not a
|
|
74119
|
+
* structural fact about the mosaic (it is not itself an object-event row);
|
|
74120
|
+
* flagged in the D476 ADR as a spot a future dedicated owner kind may suit
|
|
74121
|
+
* better. It is deliberately NOT owned by a track: a track's media list is a
|
|
74122
|
+
* LIVE viewer surface, and a cross-camera mosaic appearing in one track's
|
|
74123
|
+
* tiles would be a picture that lies about what it is of.
|
|
73352
74124
|
*/
|
|
73353
74125
|
buildSummaryPorts(stores) {
|
|
73354
74126
|
return {
|
|
@@ -73389,7 +74161,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73389
74161
|
},
|
|
73390
74162
|
storeMosaic: async ({ deviceId, ownerId, timestamp, jpeg }) => stores.mediaStore.put({
|
|
73391
74163
|
deviceId,
|
|
73392
|
-
ownerKind: "
|
|
74164
|
+
ownerKind: "object",
|
|
73393
74165
|
ownerId,
|
|
73394
74166
|
kind: "fullFrame",
|
|
73395
74167
|
timestamp,
|
|
@@ -73629,7 +74401,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73629
74401
|
listEventIdsForTracks: async (trackIds) => {
|
|
73630
74402
|
if (!eventStore || trackIds.length === 0) return [];
|
|
73631
74403
|
return (await eventStore.queryObjectByTrackIds(trackIds)).flatMap((e) => e.trackId === void 0 ? [] : [{
|
|
73632
|
-
eventId: e.id,
|
|
74404
|
+
eventId: String(e.id),
|
|
73633
74405
|
trackId: e.trackId
|
|
73634
74406
|
}]);
|
|
73635
74407
|
},
|
|
@@ -74425,9 +75197,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
74425
75197
|
})
|
|
74426
75198
|
]);
|
|
74427
75199
|
return {
|
|
74428
|
-
motion,
|
|
74429
|
-
object,
|
|
74430
|
-
audio
|
|
75200
|
+
motion: toEventLike(motion),
|
|
75201
|
+
object: toEventLike(object),
|
|
75202
|
+
audio: toEventLike(audio)
|
|
74431
75203
|
};
|
|
74432
75204
|
},
|
|
74433
75205
|
fetchMediaOwners: async ({ deviceId, ownerKind, ownerIds }) => {
|
|
@@ -74582,6 +75354,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
74582
75354
|
this.alarmPanelConvergence = null;
|
|
74583
75355
|
for (const unsub of this.subscriptionUnsubs) unsub();
|
|
74584
75356
|
this.subscriptionUnsubs = [];
|
|
75357
|
+
await this.motionEpisodes?.closeAll("addon-shutdown");
|
|
74585
75358
|
await this.embeddingDispatcher?.stop();
|
|
74586
75359
|
this.embeddingDispatcher = null;
|
|
74587
75360
|
await this.notificationCenter?.stop();
|
|
@@ -75186,11 +75959,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75186
75959
|
byState
|
|
75187
75960
|
} });
|
|
75188
75961
|
}
|
|
75189
|
-
await
|
|
75962
|
+
const objectEvents = await this.persistObjectEvents(deviceId, result.objectEvents);
|
|
75963
|
+
const appearanceEvents = await this.persistObjectEvents(deviceId, result.appearanceEvents);
|
|
75190
75964
|
if (this.notificationCenter !== null) {
|
|
75191
75965
|
const overlaps = processor.getLastZoneOverlaps();
|
|
75192
75966
|
const rejections = processor.getLastZoneRejections();
|
|
75193
|
-
for (const e of
|
|
75967
|
+
for (const e of objectEvents) {
|
|
75194
75968
|
if (e.zones && e.zones.length > 0) {
|
|
75195
75969
|
const m = e.trackId ? overlaps.get(e.trackId) : void 0;
|
|
75196
75970
|
this.ctx.logger.info("zone membership stamped on event", {
|
|
@@ -75244,7 +76018,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75244
76018
|
const faceGloballyEnabled = this.faceRecognizer ? await this.resolveGlobalFaceEnabled() : false;
|
|
75245
76019
|
const mediaSettings = await this.resolveDeviceMediaSettings(deviceId);
|
|
75246
76020
|
if (this.eventMediaDispatcher && frameHandle) {
|
|
75247
|
-
const mediaEvents = [...
|
|
76021
|
+
const mediaEvents = [...objectEvents, ...appearanceEvents];
|
|
75248
76022
|
const childCropsByEvent = buildEventChildCrops(mediaEvents, frame.detections);
|
|
75249
76023
|
const uncroppable = mediaEvents.filter((e) => e.bbox === void 0 || e.bbox.w <= 0 || e.bbox.h <= 0);
|
|
75250
76024
|
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)", {
|
|
@@ -75258,7 +76032,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75258
76032
|
const eventTargets = mediaEvents.filter((e) => e.bbox !== void 0 && e.bbox.w > 0 && e.bbox.h > 0).map((e) => {
|
|
75259
76033
|
const childCrops = childCropsByEvent.get(e.id);
|
|
75260
76034
|
return {
|
|
75261
|
-
eventId: e.id,
|
|
76035
|
+
eventId: String(e.id),
|
|
75262
76036
|
timestamp: e.timestamp,
|
|
75263
76037
|
bbox: e.bbox,
|
|
75264
76038
|
className: e.className,
|
|
@@ -75412,7 +76186,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75412
76186
|
...frameHandle !== void 0 ? { frameHandle } : {}
|
|
75413
76187
|
});
|
|
75414
76188
|
if (objectEmbeddingBests.length > 0) this.persistObjectEmbeddingBests(deviceId, result.timestamp, objectEmbeddingBests, frameHandle, result.frameWidth, result.frameHeight, mediaSettings.cropPadding);
|
|
75415
|
-
for (const e of
|
|
76189
|
+
for (const e of objectEvents) this.ctx.eventBus.emit({
|
|
75416
76190
|
id: `pa-${e.id}`,
|
|
75417
76191
|
timestamp: new Date(e.timestamp),
|
|
75418
76192
|
source: {
|
|
@@ -75510,6 +76284,45 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75510
76284
|
* eight settings kinds funnel through on a cache miss, so calling the raw
|
|
75511
76285
|
* reads directly here means eight kinds expiring in the same window each
|
|
75512
76286
|
* pay for the same two reads — see `store/settings-raw-mirror.ts`. */
|
|
76287
|
+
/**
|
|
76288
|
+
* Insert the frame's object-event DRAFTS and return them with the id the
|
|
76289
|
+
* database actually assigned (D474).
|
|
76290
|
+
*
|
|
76291
|
+
* The frame path cannot name an event: the id is a SQLite rowid, minted by
|
|
76292
|
+
* the write. So this is the ONE place a draft becomes an `ObjectEvent`, and
|
|
76293
|
+
* everything downstream — the media targets, the notification-centre hook,
|
|
76294
|
+
* the bus emit — consumes what comes back rather than what went in.
|
|
76295
|
+
*
|
|
76296
|
+
* Order is preserved. A draft whose row did not land is DROPPED and SAID:
|
|
76297
|
+
* it owns no media, can be named on no bus and can match no rule, and an
|
|
76298
|
+
* invented id would put `event:NaN` in a media key. A branch that accepts
|
|
76299
|
+
* work and produces nothing must log it.
|
|
76300
|
+
*/
|
|
76301
|
+
async persistObjectEvents(deviceId, drafts) {
|
|
76302
|
+
if (drafts.length === 0) return [];
|
|
76303
|
+
const store = this.eventStore;
|
|
76304
|
+
if (store === null) return [];
|
|
76305
|
+
const ids = await Promise.all(drafts.map((d) => store.insertObject(d)));
|
|
76306
|
+
const persisted = [];
|
|
76307
|
+
for (const [i, draft] of drafts.entries()) {
|
|
76308
|
+
const id = ids[i];
|
|
76309
|
+
if (id === void 0 || id === null) continue;
|
|
76310
|
+
persisted.push({
|
|
76311
|
+
...draft,
|
|
76312
|
+
id
|
|
76313
|
+
});
|
|
76314
|
+
}
|
|
76315
|
+
const lost = drafts.length - persisted.length;
|
|
76316
|
+
if (lost > 0) this.ctx.logger.warn("object events did not persist — no id, so no media, no bus, no rule", {
|
|
76317
|
+
tags: { deviceId },
|
|
76318
|
+
meta: {
|
|
76319
|
+
lost,
|
|
76320
|
+
of: drafts.length,
|
|
76321
|
+
trackIds: [...new Set(drafts.map((d) => d.trackId ?? "none"))]
|
|
76322
|
+
}
|
|
76323
|
+
});
|
|
76324
|
+
return persisted;
|
|
76325
|
+
}
|
|
75513
76326
|
async readDeviceSettings(deviceId, resolve) {
|
|
75514
76327
|
const globalRaw = this.ctxIfReady !== null ? await this.rawGlobalStoreMirror.resolve() : {};
|
|
75515
76328
|
const deviceRaw = await this.rawDeviceBlobMirror.resolve(deviceId);
|
|
@@ -76288,7 +77101,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
76288
77101
|
recognized: this.recognizedTrackIds.has(t.trackId)
|
|
76289
77102
|
}))
|
|
76290
77103
|
});
|
|
76291
|
-
|
|
77104
|
+
const frameRef = { timestamp: result.timestamp };
|
|
77105
|
+
const worthPixels = this.groupShots.offer(open.id, score, frameRef);
|
|
77106
|
+
this.logGroupShotWindow(deviceId);
|
|
77107
|
+
if (!worthPixels) return;
|
|
76292
77108
|
const getNative = this.getNativeKeyFrameRgb;
|
|
76293
77109
|
const mediaStore = this.mediaStore;
|
|
76294
77110
|
if (!getNative || !mediaStore) return;
|
|
@@ -76310,17 +77126,48 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
76310
77126
|
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
|
|
76311
77127
|
})
|
|
76312
77128
|
}).then((outcome) => {
|
|
76313
|
-
if (outcome?.kind === "landed")
|
|
76314
|
-
|
|
76315
|
-
|
|
76316
|
-
|
|
76317
|
-
|
|
76318
|
-
|
|
76319
|
-
|
|
76320
|
-
|
|
77129
|
+
if (outcome?.kind === "landed") {
|
|
77130
|
+
this.groupShots.noteLanded(summaryId, score, frameRef);
|
|
77131
|
+
this.ctx.logger.debug("summary group shot landed", {
|
|
77132
|
+
tags: { deviceId },
|
|
77133
|
+
meta: {
|
|
77134
|
+
summaryId,
|
|
77135
|
+
visible: score.visible,
|
|
77136
|
+
quality: Number(score.quality.toFixed(3))
|
|
77137
|
+
}
|
|
77138
|
+
});
|
|
77139
|
+
}
|
|
76321
77140
|
}).catch(() => {});
|
|
76322
77141
|
}
|
|
76323
77142
|
/**
|
|
77143
|
+
* The group-shot gate's window, throttled to one line per
|
|
77144
|
+
* {@link GROUP_SHOT_METRIC_INTERVAL_MS}. Process-global counts, tagged with
|
|
77145
|
+
* the device that happened to close the window — the counters are the
|
|
77146
|
+
* gate's, not the camera's, and the tag is there because every line about a
|
|
77147
|
+
* device carries one.
|
|
77148
|
+
*
|
|
77149
|
+
* `materialised` is the number of 4096-px native full-frame fetches this
|
|
77150
|
+
* path actually spent; `byGap` + `byMargin` + `byWorse` is what it did not.
|
|
77151
|
+
*/
|
|
77152
|
+
logGroupShotWindow(deviceId) {
|
|
77153
|
+
const now = Date.now();
|
|
77154
|
+
if (now - this.lastGroupShotMetricAt < GROUP_SHOT_METRIC_INTERVAL_MS) return;
|
|
77155
|
+
this.lastGroupShotMetricAt = now;
|
|
77156
|
+
const counts = this.groupShots.drainCounts();
|
|
77157
|
+
if (counts.offered === 0) return;
|
|
77158
|
+
this.ctx.logger.info("group shot window", {
|
|
77159
|
+
tags: { deviceId },
|
|
77160
|
+
meta: {
|
|
77161
|
+
offered: counts.offered,
|
|
77162
|
+
materialised: counts.materialised,
|
|
77163
|
+
suppressedByGap: counts.byGap,
|
|
77164
|
+
suppressedByMargin: counts.byMargin,
|
|
77165
|
+
suppressedByWorse: counts.byWorse,
|
|
77166
|
+
openSummaries: this.groupShots.size
|
|
77167
|
+
}
|
|
77168
|
+
});
|
|
77169
|
+
}
|
|
77170
|
+
/**
|
|
76324
77171
|
* Order the parcels a best-frame decision wants (D422). Skipped entirely at
|
|
76325
77172
|
* a node the registry already called offline (D58) — logged on the
|
|
76326
77173
|
* transition by `noteCapturesGated`, counted in the key-frame window.
|
|
@@ -76866,9 +77713,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
76866
77713
|
* The two mirrors answer different halves and neither subsumes the other:
|
|
76867
77714
|
* - {@link lastTrackActivityMs} — a tracked object was alive. Present only
|
|
76868
77715
|
* while the camera is ATTACHED and producing inference frames.
|
|
76869
|
-
* - {@link
|
|
76870
|
-
* onboard). Covers a camera whose detector found nothing,
|
|
76871
|
-
* on `detectionMode: 'on-motion'` in the window before it
|
|
77716
|
+
* - {@link lastMotionTickByDevice} — motion was reported (analyzer or
|
|
77717
|
+
* onboard, ANY source). Covers a camera whose detector found nothing,
|
|
77718
|
+
* and a camera on `detectionMode: 'on-motion'` in the window before it
|
|
77719
|
+
* attaches. This is a plain per-device mirror, deliberately NOT the
|
|
77720
|
+
* per-source episode state in {@link motionEpisodes} — this question is
|
|
77721
|
+
* "is something visual happening on the camera", not "which episode".
|
|
76872
77722
|
*
|
|
76873
77723
|
* Staleness is safe in the direction that matters: a mirror that is late
|
|
76874
77724
|
* suppresses a marker, it never invents one.
|
|
@@ -76876,7 +77726,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
76876
77726
|
isMotionActive(deviceId, atMs, quietMs) {
|
|
76877
77727
|
const trackAt = this.lastTrackActivityMs.get(deviceId);
|
|
76878
77728
|
if (trackAt !== void 0 && atMs - trackAt < quietMs) return true;
|
|
76879
|
-
const motion = this.
|
|
77729
|
+
const motion = this.lastMotionTickByDevice.get(deviceId);
|
|
76880
77730
|
return motion !== void 0 && motion.detected && atMs - motion.atMs < quietMs;
|
|
76881
77731
|
}
|
|
76882
77732
|
/** Per-device audio-marker config, folded from the cached audio settings. */
|
|
@@ -76941,8 +77791,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
76941
77791
|
* therefore persisted nothing at all.
|
|
76942
77792
|
*/
|
|
76943
77793
|
async persistClassifiedAudio(input) {
|
|
76944
|
-
const
|
|
76945
|
-
id: (0, node_crypto.randomUUID)(),
|
|
77794
|
+
const draft = {
|
|
76946
77795
|
deviceId: input.deviceId,
|
|
76947
77796
|
timestamp: input.timestamp,
|
|
76948
77797
|
kind: "audio",
|
|
@@ -76958,7 +77807,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
76958
77807
|
className: input.className,
|
|
76959
77808
|
atMs: input.timestamp
|
|
76960
77809
|
});
|
|
76961
|
-
await this.eventStore.insertAudio(
|
|
77810
|
+
const insertedId = await this.eventStore.insertAudio(draft);
|
|
77811
|
+
if (insertedId === null) return;
|
|
77812
|
+
const ev = {
|
|
77813
|
+
...draft,
|
|
77814
|
+
id: insertedId
|
|
77815
|
+
};
|
|
76962
77816
|
this.notificationCenter?.onAudioEventPersisted(ev);
|
|
76963
77817
|
this.trackStore?.addAudioLabelEpisode(input.deviceId, input.className, input.score, input.timestamp);
|
|
76964
77818
|
this.ctx.eventBus.emit({
|
|
@@ -77169,8 +78023,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77169
78023
|
});
|
|
77170
78024
|
this.levelStateByDevice.set(deviceId, res.nextState);
|
|
77171
78025
|
if (!res.emit) return;
|
|
77172
|
-
const
|
|
77173
|
-
id: (0, node_crypto.randomUUID)(),
|
|
78026
|
+
const draft = {
|
|
77174
78027
|
deviceId,
|
|
77175
78028
|
timestamp,
|
|
77176
78029
|
kind: "audio",
|
|
@@ -77181,7 +78034,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77181
78034
|
className: void 0,
|
|
77182
78035
|
atMs: timestamp
|
|
77183
78036
|
});
|
|
77184
|
-
await this.eventStore.insertAudio(
|
|
78037
|
+
const insertedId = await this.eventStore.insertAudio(draft);
|
|
78038
|
+
if (insertedId === null) return;
|
|
78039
|
+
const ev = {
|
|
78040
|
+
...draft,
|
|
78041
|
+
id: insertedId
|
|
78042
|
+
};
|
|
77185
78043
|
this.ctx.eventBus.emit({
|
|
77186
78044
|
id: `pa-${ev.id}`,
|
|
77187
78045
|
timestamp: new Date(ev.timestamp),
|
|
@@ -77200,10 +78058,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77200
78058
|
});
|
|
77201
78059
|
}
|
|
77202
78060
|
/**
|
|
77203
|
-
* Persist motion-analysis bus events
|
|
77204
|
-
*
|
|
77205
|
-
*
|
|
77206
|
-
*
|
|
78061
|
+
* Persist motion-analysis bus events into the EPISODE tracker (D475): a
|
|
78062
|
+
* rising edge with no open episode INSERTs a row; a further edge inside an
|
|
78063
|
+
* open episode extends it in place; the episode closes on its own timer,
|
|
78064
|
+
* `MOTION_CLOSE_AFTER_MS` after the last rising edge. See
|
|
78065
|
+
* `motion-episode.ts` / `motion-episode-tracker.ts` for the state machine.
|
|
77207
78066
|
*
|
|
77208
78067
|
* Gated on the same `pipeline-analytics` wrapper binding as the
|
|
77209
78068
|
* inference + audio handlers so toggling the wrapper off for a
|
|
@@ -77213,22 +78072,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77213
78072
|
if (this.shuttingDown) return;
|
|
77214
78073
|
if (!await this.bindingCache.isActive(deviceId)) return;
|
|
77215
78074
|
const detected = payload.detected === true;
|
|
77216
|
-
|
|
77217
|
-
|
|
77218
|
-
|
|
77219
|
-
|
|
77220
|
-
|
|
77221
|
-
|
|
77222
|
-
atMs: timestamp
|
|
77223
|
-
});
|
|
77224
|
-
return;
|
|
77225
|
-
}
|
|
77226
|
-
if (!transitionedOn && !heartbeatDue) return;
|
|
77227
|
-
const ev = {
|
|
77228
|
-
id: (0, node_crypto.randomUUID)(),
|
|
77229
|
-
deviceId,
|
|
77230
|
-
timestamp,
|
|
77231
|
-
kind: "motion",
|
|
78075
|
+
this.lastMotionTickByDevice.set(deviceId, {
|
|
78076
|
+
detected,
|
|
78077
|
+
atMs: timestamp
|
|
78078
|
+
});
|
|
78079
|
+
if (detected) this.eventStore?.noteMotionSample(deviceId, payload.regionCount, timestamp);
|
|
78080
|
+
await this.motionEpisodes?.observe(deviceId, "analyzer", detected, timestamp, {
|
|
77232
78081
|
regionCount: payload.regionCount,
|
|
77233
78082
|
regions: payload.regions.map((r) => ({
|
|
77234
78083
|
bbox: {
|
|
@@ -77242,69 +78091,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77242
78091
|
})),
|
|
77243
78092
|
frameWidth: payload.frameWidth,
|
|
77244
78093
|
frameHeight: payload.frameHeight
|
|
77245
|
-
};
|
|
77246
|
-
this.lastMotionInsertByDevice.set(deviceId, {
|
|
77247
|
-
detected,
|
|
77248
|
-
atMs: timestamp
|
|
77249
|
-
});
|
|
77250
|
-
if (transitionedOn) this.motionOnsets.noteRisingEdge(deviceId, timestamp);
|
|
77251
|
-
this.sceneEngine?.noteMotion(deviceId, timestamp);
|
|
77252
|
-
await this.eventStore.insertMotion(ev);
|
|
77253
|
-
this.ctx.eventBus.emit({
|
|
77254
|
-
id: `pa-${ev.id}`,
|
|
77255
|
-
timestamp: new Date(ev.timestamp),
|
|
77256
|
-
source: {
|
|
77257
|
-
type: "addon",
|
|
77258
|
-
id: "pipeline-analytics",
|
|
77259
|
-
addonId: "pipeline-analytics"
|
|
77260
|
-
},
|
|
77261
|
-
category: require_dist.EventCategory.PipelineAnalyticsDetectionEvent,
|
|
77262
|
-
data: {
|
|
77263
|
-
deviceId,
|
|
77264
|
-
kind: "motion",
|
|
77265
|
-
eventId: ev.id,
|
|
77266
|
-
timestamp: ev.timestamp
|
|
77267
|
-
}
|
|
77268
78094
|
});
|
|
77269
|
-
if (transitionedOn) await this.captureMotionMarkerStill(deviceId, ev.id, ev.timestamp);
|
|
77270
78095
|
}
|
|
77271
78096
|
/**
|
|
77272
|
-
* Persist firmware-driven (onboard) motion events
|
|
77273
|
-
* {@link handleMotionAnalysis}
|
|
77274
|
-
*
|
|
77275
|
-
*
|
|
77276
|
-
* shape which lacks frame
|
|
77277
|
-
* (firmware reports a binary
|
|
77278
|
-
* coarse bbox via the optional
|
|
77279
|
-
*
|
|
77280
|
-
* The same `lastMotionInsertByDevice` map is shared with the analyzer
|
|
77281
|
-
* path so a camera that briefly switches `motionSources` between
|
|
77282
|
-
* `onboard` and `analyzer` gets consistent throttling — both paths
|
|
77283
|
-
* are mutually exclusive at the event-emit layer (the runner only
|
|
77284
|
-
* fires `MotionAnalysis` when its analyzer runs; onboard providers
|
|
77285
|
-
* never fire `MotionAnalysis`), so there's no double-count risk.
|
|
78097
|
+
* Persist firmware-driven (onboard / device-activity) motion events into
|
|
78098
|
+
* the SAME episode tracker as {@link handleMotionAnalysis}, keyed per
|
|
78099
|
+
* `(deviceId, source)` so the two paths can never share — or fight over —
|
|
78100
|
+
* one episode on a camera whose `motionSources` lists both (D475). Reads
|
|
78101
|
+
* from the {@link MotionOnMotionChangedPayload} shape which lacks frame
|
|
78102
|
+
* dimensions and pixel-level region details (firmware reports a binary
|
|
78103
|
+
* detected flag plus, on rare devices, a coarse bbox via the optional
|
|
78104
|
+
* `regions` field).
|
|
77286
78105
|
*/
|
|
77287
78106
|
async handleOnboardMotion(deviceId, payload, timestamp) {
|
|
77288
78107
|
if (this.shuttingDown) return;
|
|
77289
78108
|
if (!await this.bindingCache.isActive(deviceId)) return;
|
|
77290
78109
|
const detected = payload.detected === true;
|
|
77291
|
-
|
|
77292
|
-
|
|
77293
|
-
|
|
77294
|
-
|
|
77295
|
-
if (last?.detected === true) this.lastMotionInsertByDevice.set(deviceId, {
|
|
77296
|
-
detected: false,
|
|
77297
|
-
atMs: timestamp
|
|
77298
|
-
});
|
|
77299
|
-
return;
|
|
77300
|
-
}
|
|
77301
|
-
if (!transitionedOn && !heartbeatDue) return;
|
|
78110
|
+
this.lastMotionTickByDevice.set(deviceId, {
|
|
78111
|
+
detected,
|
|
78112
|
+
atMs: timestamp
|
|
78113
|
+
});
|
|
77302
78114
|
const regions = payload.regions ?? [];
|
|
77303
|
-
|
|
77304
|
-
|
|
77305
|
-
deviceId,
|
|
77306
|
-
timestamp,
|
|
77307
|
-
kind: "motion",
|
|
78115
|
+
if (detected) this.eventStore?.noteMotionSample(deviceId, regions.length, timestamp);
|
|
78116
|
+
await this.motionEpisodes?.observe(deviceId, payload.source, detected, timestamp, {
|
|
77308
78117
|
regionCount: regions.length,
|
|
77309
78118
|
regions: regions.map((r) => ({
|
|
77310
78119
|
bbox: {
|
|
@@ -77318,17 +78127,56 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77318
78127
|
})),
|
|
77319
78128
|
frameWidth: 0,
|
|
77320
78129
|
frameHeight: 0
|
|
78130
|
+
});
|
|
78131
|
+
}
|
|
78132
|
+
/**
|
|
78133
|
+
* `MotionEpisodeTracker.insertRow` — builds and inserts the row that opens
|
|
78134
|
+
* a motion episode. `regionData` carries the per-kind fields the two motion
|
|
78135
|
+
* handlers differ on (regions/frameWidth/frameHeight); everything else
|
|
78136
|
+
* (id, durationMs, edges) is the episode's own concern, not the caller's.
|
|
78137
|
+
*/
|
|
78138
|
+
async insertMotionEpisodeRow(deviceId, timestamp, regionData) {
|
|
78139
|
+
const draft = {
|
|
78140
|
+
deviceId,
|
|
78141
|
+
timestamp,
|
|
78142
|
+
kind: "motion",
|
|
78143
|
+
durationMs: null,
|
|
78144
|
+
...regionData
|
|
77321
78145
|
};
|
|
77322
|
-
this.
|
|
77323
|
-
|
|
77324
|
-
|
|
78146
|
+
return this.eventStore.insertMotion(draft);
|
|
78147
|
+
}
|
|
78148
|
+
/**
|
|
78149
|
+
* `MotionEpisodeTracker.closeRow` — the UPDATE that closes an episode.
|
|
78150
|
+
*/
|
|
78151
|
+
async closeMotionEpisodeRow(input) {
|
|
78152
|
+
return this.eventStore.updateMotion(input.eventId, input.deviceId, {
|
|
78153
|
+
durationMs: input.durationMs,
|
|
78154
|
+
edges: input.edges
|
|
78155
|
+
});
|
|
78156
|
+
}
|
|
78157
|
+
/**
|
|
78158
|
+
* `MotionEpisodeTracker.onNewEdge` — the side effects the pre-D475 code
|
|
78159
|
+
* fired on every `transitionedOn`: the birth-latency onset mark (D409, only
|
|
78160
|
+
* the FIRST edge of a burst moves it — a further edge inside the same
|
|
78161
|
+
* episode is a fresh onset in its own right, so it moves too) and the
|
|
78162
|
+
* motion-marker still (self-throttled by `motion-marker-policy.ts`;
|
|
78163
|
+
* `String` because a media key is a URL component while the id itself
|
|
78164
|
+
* stays a number, D474).
|
|
78165
|
+
*/
|
|
78166
|
+
onMotionEpisodeEdge(input) {
|
|
78167
|
+
this.motionOnsets.noteRisingEdge(input.deviceId, input.timestamp);
|
|
78168
|
+
this.captureMotionMarkerStill(input.deviceId, String(input.eventId), input.timestamp).catch((err) => {
|
|
78169
|
+
this.ctx.logger.debug("motion marker still capture failed", {
|
|
78170
|
+
tags: { deviceId: input.deviceId },
|
|
78171
|
+
meta: {
|
|
78172
|
+
eventId: input.eventId,
|
|
78173
|
+
error: require_dist.errMsg(err)
|
|
78174
|
+
}
|
|
78175
|
+
});
|
|
77325
78176
|
});
|
|
77326
|
-
if (transitionedOn) this.motionOnsets.noteRisingEdge(deviceId, timestamp);
|
|
77327
|
-
this.sceneEngine?.noteMotion(deviceId, timestamp);
|
|
77328
|
-
await this.eventStore.insertMotion(ev);
|
|
77329
78177
|
this.ctx.eventBus.emit({
|
|
77330
|
-
id: `pa-${
|
|
77331
|
-
timestamp: new Date(
|
|
78178
|
+
id: `pa-${input.eventId}`,
|
|
78179
|
+
timestamp: new Date(input.timestamp),
|
|
77332
78180
|
source: {
|
|
77333
78181
|
type: "addon",
|
|
77334
78182
|
id: "pipeline-analytics",
|
|
@@ -77336,13 +78184,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77336
78184
|
},
|
|
77337
78185
|
category: require_dist.EventCategory.PipelineAnalyticsDetectionEvent,
|
|
77338
78186
|
data: {
|
|
77339
|
-
deviceId,
|
|
78187
|
+
deviceId: input.deviceId,
|
|
77340
78188
|
kind: "motion",
|
|
77341
|
-
eventId:
|
|
77342
|
-
timestamp:
|
|
78189
|
+
eventId: input.eventId,
|
|
78190
|
+
timestamp: input.timestamp
|
|
77343
78191
|
}
|
|
77344
78192
|
});
|
|
77345
|
-
|
|
78193
|
+
}
|
|
78194
|
+
/**
|
|
78195
|
+
* `MotionEpisodeTracker.onTick` — fires on every `detected:true`
|
|
78196
|
+
* observation that reaches an open or newly-opened episode, transition or
|
|
78197
|
+
* continuation. Arms the scene quiet-edge trigger: a scene is checked
|
|
78198
|
+
* `quietSeconds` AFTER the last motion, never during it, so this wants
|
|
78199
|
+
* "motion is still happening" — the SAME question `onNewEdge` does not
|
|
78200
|
+
* answer, since a long continuous burst is mostly continuation ticks.
|
|
78201
|
+
*/
|
|
78202
|
+
onMotionEpisodeTick(input) {
|
|
78203
|
+
this.sceneEngine?.noteMotion(input.deviceId, input.timestamp);
|
|
77346
78204
|
}
|
|
77347
78205
|
/**
|
|
77348
78206
|
* The gate in front of the motion still. Both motion handlers reach it, so
|
|
@@ -79910,7 +80768,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
79910
80768
|
deps: {
|
|
79911
80769
|
page: ({ deviceId, scope, afterMs, limit }) => mediaStore.pageByKindForDevice({
|
|
79912
80770
|
deviceId,
|
|
79913
|
-
ownerKind: scope === "motion-still" ? "
|
|
80771
|
+
ownerKind: scope === "motion-still" ? "motion" : "track",
|
|
79914
80772
|
kind: "snapshot",
|
|
79915
80773
|
afterMs,
|
|
79916
80774
|
limit
|
|
@@ -80060,7 +80918,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
80060
80918
|
for (const { event, score } of bestEventByTrackId.values()) {
|
|
80061
80919
|
const mediaUrl = resolveSearchThumbnailUrl({
|
|
80062
80920
|
baseUrl: this.eventMediaBaseUrl,
|
|
80063
|
-
eventId: event.id,
|
|
80921
|
+
eventId: String(event.id),
|
|
80064
80922
|
...event.trackId !== void 0 && embeddingMediaKeyByTrackId.has(event.trackId) ? { embeddingMediaKey: embeddingMediaKeyByTrackId.get(event.trackId) } : {}
|
|
80065
80923
|
});
|
|
80066
80924
|
const keyFrameMediaKey = event.trackId !== void 0 ? keyFrameKeyByTrackId.get(event.trackId) : void 0;
|
|
@@ -80659,12 +81517,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
80659
81517
|
deviceId: file.deviceId
|
|
80660
81518
|
};
|
|
80661
81519
|
};
|
|
80662
|
-
|
|
81520
|
+
let eventFiles = [];
|
|
81521
|
+
for (const ownerType of require_dist.EVENT_OWNER_TYPES) {
|
|
81522
|
+
eventFiles = await listInfo(ownerType, id);
|
|
81523
|
+
if (eventFiles.length > 0) break;
|
|
81524
|
+
}
|
|
80663
81525
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
80664
81526
|
const layers = await collectCropForcedMedia({
|
|
80665
81527
|
listByOwner: listInfo,
|
|
80666
81528
|
getTrackIdForEvent
|
|
80667
|
-
}, id);
|
|
81529
|
+
}, id, eventFiles);
|
|
80668
81530
|
const bodyKeys = new Set(layers.bodyFiles.map((f) => f.key));
|
|
80669
81531
|
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);
|
|
80670
81532
|
}
|