@camstack/addon-post-analysis 1.2.85 → 1.2.86
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/pipeline-analytics/index.js +147 -27
- package/dist/pipeline-analytics/index.mjs +147 -27
- package/package.json +1 -1
|
@@ -5715,7 +5715,7 @@ var NcAudioStillShelf = class {
|
|
|
5715
5715
|
}
|
|
5716
5716
|
async fetch(captureId, deviceId, atMs) {
|
|
5717
5717
|
try {
|
|
5718
|
-
const shot = await withTimeout$
|
|
5718
|
+
const shot = await withTimeout$3(this.deps.getSnapshot(deviceId), SNAPSHOT_TIMEOUT_MS);
|
|
5719
5719
|
if (shot === null || shot.base64.length === 0) {
|
|
5720
5720
|
this.reportMiss(deviceId, "the camera returned no snapshot");
|
|
5721
5721
|
return;
|
|
@@ -5781,7 +5781,7 @@ function audioStillMedia(data, timestamp) {
|
|
|
5781
5781
|
];
|
|
5782
5782
|
}
|
|
5783
5783
|
/** Reject if `promise` does not settle within `ms`. */
|
|
5784
|
-
function withTimeout$
|
|
5784
|
+
function withTimeout$3(promise, ms) {
|
|
5785
5785
|
return new Promise((resolve, reject) => {
|
|
5786
5786
|
const timer = setTimeout(() => {
|
|
5787
5787
|
reject(/* @__PURE__ */ new Error(`snapshot cap timed out after ${String(ms)}ms`));
|
|
@@ -30485,7 +30485,7 @@ var undecided = (candidate, reason) => ({
|
|
|
30485
30485
|
reason,
|
|
30486
30486
|
className: candidate.className
|
|
30487
30487
|
});
|
|
30488
|
-
function withTimeout$
|
|
30488
|
+
function withTimeout$2(promise, timeoutMs) {
|
|
30489
30489
|
return new Promise((resolve, reject) => {
|
|
30490
30490
|
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("confirmation-timeout")), timeoutMs);
|
|
30491
30491
|
promise.then((value) => {
|
|
@@ -30525,7 +30525,7 @@ async function confirmOne(candidate, config, deps, exhausted) {
|
|
|
30525
30525
|
const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
|
|
30526
30526
|
if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
|
|
30527
30527
|
try {
|
|
30528
|
-
return await withTimeout$
|
|
30528
|
+
return await withTimeout$2(runConfirmation(candidate, config, deps, exhausted), config.timeoutMs);
|
|
30529
30529
|
} catch {
|
|
30530
30530
|
return undecided(candidate, "timeout");
|
|
30531
30531
|
}
|
|
@@ -34772,7 +34772,8 @@ function isMediaOwnerKind(value) {
|
|
|
34772
34772
|
* write="one row per blob written through put / putReplacing — a track's
|
|
34773
34773
|
* keyFrame, thumbnail, firstFrame and lastFrame (UPSERTED on a deterministic
|
|
34774
34774
|
* id, so one per track+kind however many captures race), its periodic
|
|
34775
|
-
* snapshots, each event's crop and full frames,
|
|
34775
|
+
* snapshots, each event's crop and full frames, a motion visit's still
|
|
34776
|
+
* (`kind: snapshot` on ownerKind event), and the buffered face/plate
|
|
34776
34777
|
* crops. A row is also REOWNED here (not rewritten) when the operator enrols a
|
|
34777
34778
|
* face or a plate, which is what moves it out of retention's reach."
|
|
34778
34779
|
* retention="no age sweep of its own — MediaStore.evictBefore was deleted (D59)
|
|
@@ -39078,11 +39079,14 @@ function tieredLabelColumnData(patch) {
|
|
|
39078
39079
|
* (MOTION_EVENT_HEARTBEAT_MS) while it stays on. The analyzer path and the
|
|
39079
39080
|
* onboard-firmware path share that throttle, so a camera cannot double-count.
|
|
39080
39081
|
* Measured at ~7,200 rows/day/camera."
|
|
39081
|
-
* retention="a TERMINAL age-keyed root — no trackId
|
|
39082
|
-
*
|
|
39082
|
+
* retention="a TERMINAL age-keyed root — no trackId. A motion row may own a
|
|
39083
|
+
* still (`ownerKind: 'event'`, `kind: 'snapshot'`) captured on the off→on
|
|
39084
|
+
* visit so events-mode recordings have a videoclip thumbnail. Deleted by
|
|
39083
39085
|
* EventStore.evictTracklessBefore on the DEVICE's one retention window
|
|
39084
|
-
* (follow-recordings horizon, or trackRetentionDays, default 7)
|
|
39085
|
-
*
|
|
39086
|
+
* (follow-recordings horizon, or trackRetentionDays, default 7), which
|
|
39087
|
+
* folds in MediaStore.deleteForEvents so the still leaves with the row.
|
|
39088
|
+
* 0 days = keep forever, and this table is the reason that setting is
|
|
39089
|
+
* dangerous."
|
|
39086
39090
|
*/
|
|
39087
39091
|
var MOTION_EVENTS_COLLECTION = "pipeline-analytics:motion-events";
|
|
39088
39092
|
/**
|
|
@@ -39811,9 +39815,10 @@ var EventStore = class {
|
|
|
39811
39815
|
* Object events now leave exactly one way: with their track, through
|
|
39812
39816
|
* {@link deleteByTracks} (D61).
|
|
39813
39817
|
*
|
|
39814
|
-
* Motion and audio carry no `trackId
|
|
39815
|
-
*
|
|
39816
|
-
*
|
|
39818
|
+
* Motion and audio carry no `trackId` and are referenced by nothing — they
|
|
39819
|
+
* are terminal retention ROOTS, so an age window on them cannot contradict
|
|
39820
|
+
* anybody's (`retention/retention-model.ts`). A motion visit still is
|
|
39821
|
+
* event-owned media of that row and is deleted here in lockstep.
|
|
39817
39822
|
*
|
|
39818
39823
|
* **ONE cutoff for both kinds** (D62). They used to take a window each
|
|
39819
39824
|
* (`retentionMotionDays` / `retentionAudioDays`), which is two clocks over
|
|
@@ -39830,9 +39835,17 @@ var EventStore = class {
|
|
|
39830
39835
|
...params.deviceId !== void 0 ? { where: { deviceId: params.deviceId } } : {},
|
|
39831
39836
|
whereBetween: { timestamp: [0, params.cutoffMs] }
|
|
39832
39837
|
});
|
|
39838
|
+
const motion = await batch(MOTION_EVENTS_COLLECTION);
|
|
39839
|
+
const audio = await batch(AUDIO_EVENTS_COLLECTION);
|
|
39840
|
+
const ids = [...motion, ...audio];
|
|
39841
|
+
if (ids.length > 0 && this.media !== void 0) try {
|
|
39842
|
+
await this.media.deleteForEvents(ids);
|
|
39843
|
+
} catch (err) {
|
|
39844
|
+
this.logger.warn("EventStore.evictTracklessBefore: event media delete failed", { meta: { error: String(err) } });
|
|
39845
|
+
}
|
|
39833
39846
|
return {
|
|
39834
|
-
motion
|
|
39835
|
-
audio
|
|
39847
|
+
motion,
|
|
39848
|
+
audio
|
|
39836
39849
|
};
|
|
39837
39850
|
}
|
|
39838
39851
|
/**
|
|
@@ -39857,23 +39870,32 @@ var EventStore = class {
|
|
|
39857
39870
|
* The SUBSET of `eventIds` that still exist — the orphan audit's liveness
|
|
39858
39871
|
* probe for event-owned media.
|
|
39859
39872
|
*
|
|
39873
|
+
* Motion and audio rows can own stills (`kind: 'snapshot'` on a motion
|
|
39874
|
+
* visit); probing only the object collection would classify those stills
|
|
39875
|
+
* as orphans the moment they were written.
|
|
39876
|
+
*
|
|
39860
39877
|
* THROWS on a query failure rather than answering partially. A probe that
|
|
39861
39878
|
* quietly under-reports here reads as "these events are gone", and the caller
|
|
39862
39879
|
* deletes live crops on the strength of it.
|
|
39863
39880
|
*/
|
|
39864
39881
|
async existingEventIds(eventIds) {
|
|
39865
39882
|
const found = /* @__PURE__ */ new Set();
|
|
39883
|
+
const collections = [
|
|
39884
|
+
OBJECT_EVENTS_COLLECTION,
|
|
39885
|
+
MOTION_EVENTS_COLLECTION,
|
|
39886
|
+
AUDIO_EVENTS_COLLECTION
|
|
39887
|
+
];
|
|
39866
39888
|
for (let i = 0; i < eventIds.length; i += 500) {
|
|
39867
39889
|
const chunk = eventIds.slice(i, i + 500);
|
|
39868
39890
|
if (chunk.length === 0) continue;
|
|
39869
|
-
const
|
|
39870
|
-
collection
|
|
39891
|
+
const pages = await Promise.all(collections.map((collection) => this.store.query.query({
|
|
39892
|
+
collection,
|
|
39871
39893
|
filter: {
|
|
39872
39894
|
whereIn: { id: [...chunk] },
|
|
39873
39895
|
limit: chunk.length
|
|
39874
39896
|
}
|
|
39875
|
-
});
|
|
39876
|
-
for (const row of rows) found.add(row.id);
|
|
39897
|
+
})));
|
|
39898
|
+
for (const rows of pages) for (const row of rows) found.add(row.id);
|
|
39877
39899
|
}
|
|
39878
39900
|
return found;
|
|
39879
39901
|
}
|
|
@@ -46731,16 +46753,16 @@ var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
|
46731
46753
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
46732
46754
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
46733
46755
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
46734
|
-
var SnapshotTimeoutError = class extends Error {
|
|
46756
|
+
var SnapshotTimeoutError$1 = class extends Error {
|
|
46735
46757
|
constructor(ms) {
|
|
46736
46758
|
super(`snapshot cap timed out after ${ms}ms`);
|
|
46737
46759
|
this.name = "SnapshotTimeoutError";
|
|
46738
46760
|
}
|
|
46739
46761
|
};
|
|
46740
46762
|
/** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
|
|
46741
|
-
function withTimeout(promise, ms) {
|
|
46763
|
+
function withTimeout$1(promise, ms) {
|
|
46742
46764
|
return new Promise((resolve, reject) => {
|
|
46743
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
46765
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError$1(ms)), ms);
|
|
46744
46766
|
promise.then((value) => {
|
|
46745
46767
|
clearTimeout(timer);
|
|
46746
46768
|
resolve(value);
|
|
@@ -46795,11 +46817,11 @@ var SyntheticTrackMaterializer = class {
|
|
|
46795
46817
|
*/
|
|
46796
46818
|
async fetchSnapshot(cameraId, timeoutMs) {
|
|
46797
46819
|
try {
|
|
46798
|
-
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
46820
|
+
const snap = await withTimeout$1(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
46799
46821
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"), cameraId);
|
|
46800
46822
|
return snap;
|
|
46801
46823
|
} catch (err) {
|
|
46802
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
46824
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError$1 ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
46803
46825
|
return null;
|
|
46804
46826
|
}
|
|
46805
46827
|
}
|
|
@@ -46865,6 +46887,66 @@ var SyntheticTrackMaterializer = class {
|
|
|
46865
46887
|
return track;
|
|
46866
46888
|
}
|
|
46867
46889
|
};
|
|
46890
|
+
var SnapshotTimeoutError = class extends Error {
|
|
46891
|
+
constructor(ms) {
|
|
46892
|
+
super(`snapshot cap timed out after ${ms}ms`);
|
|
46893
|
+
this.name = "SnapshotTimeoutError";
|
|
46894
|
+
}
|
|
46895
|
+
};
|
|
46896
|
+
function withTimeout(promise, ms) {
|
|
46897
|
+
return new Promise((resolve, reject) => {
|
|
46898
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
46899
|
+
promise.then((value) => {
|
|
46900
|
+
clearTimeout(timer);
|
|
46901
|
+
resolve(value);
|
|
46902
|
+
}, (err) => {
|
|
46903
|
+
clearTimeout(timer);
|
|
46904
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
46905
|
+
});
|
|
46906
|
+
});
|
|
46907
|
+
}
|
|
46908
|
+
var MotionEventSnapshotCapturer = class {
|
|
46909
|
+
deps;
|
|
46910
|
+
snapshotTimeoutMs;
|
|
46911
|
+
constructor(deps) {
|
|
46912
|
+
this.deps = deps;
|
|
46913
|
+
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
46914
|
+
}
|
|
46915
|
+
/**
|
|
46916
|
+
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
46917
|
+
* throws — the durable record is the motion row, already inserted by
|
|
46918
|
+
* the caller.
|
|
46919
|
+
*/
|
|
46920
|
+
async capture(input) {
|
|
46921
|
+
let snap;
|
|
46922
|
+
try {
|
|
46923
|
+
snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
46924
|
+
} catch (err) {
|
|
46925
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
46926
|
+
return;
|
|
46927
|
+
}
|
|
46928
|
+
if (snap === null) return;
|
|
46929
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
46930
|
+
let data = raw;
|
|
46931
|
+
try {
|
|
46932
|
+
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
46933
|
+
} catch (err) {
|
|
46934
|
+
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
46935
|
+
}
|
|
46936
|
+
try {
|
|
46937
|
+
await this.deps.media.put({
|
|
46938
|
+
deviceId: input.deviceId,
|
|
46939
|
+
ownerKind: "event",
|
|
46940
|
+
ownerId: input.eventId,
|
|
46941
|
+
kind: "snapshot",
|
|
46942
|
+
timestamp: input.timestamp,
|
|
46943
|
+
data
|
|
46944
|
+
});
|
|
46945
|
+
} catch (err) {
|
|
46946
|
+
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
46947
|
+
}
|
|
46948
|
+
}
|
|
46949
|
+
};
|
|
46868
46950
|
var TrackingSettingsSchema = require_dist.object({
|
|
46869
46951
|
/**
|
|
46870
46952
|
* How much of a detection's box must lie inside a zone (0-1 fraction of the
|
|
@@ -49835,6 +49917,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
49835
49917
|
/** The ONE projector of non-spatial events into synthetic snapshot-carrying
|
|
49836
49918
|
* tracks — linked sensor/control state changes AND audio markers. */
|
|
49837
49919
|
syntheticTracks = null;
|
|
49920
|
+
/** Still for a motion row that has no track (events-mode recording without
|
|
49921
|
+
* a detection). Lands as event-owned `kind: 'snapshot'` so videoclip
|
|
49922
|
+
* thumbnails resolve through the existing event-media plane. */
|
|
49923
|
+
motionEventSnapshots = null;
|
|
49838
49924
|
/** Decides which audio frames deserve a synthetic marker (D62). */
|
|
49839
49925
|
audioMarkers = null;
|
|
49840
49926
|
faceStore = null;
|
|
@@ -51028,6 +51114,17 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
51028
51114
|
} })
|
|
51029
51115
|
});
|
|
51030
51116
|
this.syntheticTracks = syntheticTracks;
|
|
51117
|
+
this.motionEventSnapshots = new MotionEventSnapshotCapturer({
|
|
51118
|
+
snapshot: { getSnapshot: (i) => api.snapshot?.getSnapshot.query(i) ?? Promise.resolve(null) },
|
|
51119
|
+
media: mediaStore,
|
|
51120
|
+
onError: (scope, err, deviceId) => logger.warn("motion event snapshot failed", {
|
|
51121
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
51122
|
+
meta: {
|
|
51123
|
+
scope,
|
|
51124
|
+
error: require_dist.errMsg(err)
|
|
51125
|
+
}
|
|
51126
|
+
})
|
|
51127
|
+
});
|
|
51031
51128
|
this.audioMarkers = new AudioMarkerProjector({
|
|
51032
51129
|
sink: syntheticTracks,
|
|
51033
51130
|
isMotionActive: (deviceId, atMs, quietMs) => this.isMotionActive(deviceId, atMs, quietMs),
|
|
@@ -54098,7 +54195,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54098
54195
|
const last = this.lastMotionInsertByDevice.get(deviceId);
|
|
54099
54196
|
const heartbeatDue = !last || timestamp - last.atMs >= MOTION_EVENT_HEARTBEAT_MS;
|
|
54100
54197
|
const transitionedOn = detected && (last === void 0 || last.detected === false);
|
|
54101
|
-
if (!detected)
|
|
54198
|
+
if (!detected) {
|
|
54199
|
+
if (last?.detected === true) this.lastMotionInsertByDevice.set(deviceId, {
|
|
54200
|
+
detected: false,
|
|
54201
|
+
atMs: timestamp
|
|
54202
|
+
});
|
|
54203
|
+
return;
|
|
54204
|
+
}
|
|
54102
54205
|
if (!transitionedOn && !heartbeatDue) return;
|
|
54103
54206
|
const ev = {
|
|
54104
54207
|
id: (0, node_crypto.randomUUID)(),
|
|
@@ -54141,6 +54244,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54141
54244
|
timestamp: ev.timestamp
|
|
54142
54245
|
}
|
|
54143
54246
|
});
|
|
54247
|
+
if (transitionedOn) await this.motionEventSnapshots?.capture({
|
|
54248
|
+
deviceId,
|
|
54249
|
+
eventId: ev.id,
|
|
54250
|
+
timestamp: ev.timestamp
|
|
54251
|
+
});
|
|
54144
54252
|
}
|
|
54145
54253
|
/**
|
|
54146
54254
|
* Persist firmware-driven (onboard) motion events. Mirrors
|
|
@@ -54165,7 +54273,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54165
54273
|
const last = this.lastMotionInsertByDevice.get(deviceId);
|
|
54166
54274
|
const heartbeatDue = !last || timestamp - last.atMs >= MOTION_EVENT_HEARTBEAT_MS;
|
|
54167
54275
|
const transitionedOn = detected && (last === void 0 || last.detected === false);
|
|
54168
|
-
if (!detected)
|
|
54276
|
+
if (!detected) {
|
|
54277
|
+
if (last?.detected === true) this.lastMotionInsertByDevice.set(deviceId, {
|
|
54278
|
+
detected: false,
|
|
54279
|
+
atMs: timestamp
|
|
54280
|
+
});
|
|
54281
|
+
return;
|
|
54282
|
+
}
|
|
54169
54283
|
if (!transitionedOn && !heartbeatDue) return;
|
|
54170
54284
|
const regions = payload.regions ?? [];
|
|
54171
54285
|
const ev = {
|
|
@@ -54209,6 +54323,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54209
54323
|
timestamp: ev.timestamp
|
|
54210
54324
|
}
|
|
54211
54325
|
});
|
|
54326
|
+
if (transitionedOn) await this.motionEventSnapshots?.capture({
|
|
54327
|
+
deviceId,
|
|
54328
|
+
eventId: ev.id,
|
|
54329
|
+
timestamp: ev.timestamp
|
|
54330
|
+
});
|
|
54212
54331
|
}
|
|
54213
54332
|
async sweepExpiredTracks() {
|
|
54214
54333
|
if (this.shuttingDown || !this.trackStore) return;
|
|
@@ -54277,8 +54396,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54277
54396
|
*
|
|
54278
54397
|
* 1. the track sweep, per device, cascading;
|
|
54279
54398
|
* 2. the TRACKLESS roots — motion, audio and sensor events, which carry no
|
|
54280
|
-
* trackId
|
|
54281
|
-
*
|
|
54399
|
+
* trackId and are referenced by nothing, so the cascade can never reach
|
|
54400
|
+
* them and a clock is the only thing that can. Motion visit stills leave
|
|
54401
|
+
* with their row inside `evictTracklessBefore`.
|
|
54282
54402
|
*
|
|
54283
54403
|
* That clock used to be three more settings (`retentionMotionDays` /
|
|
54284
54404
|
* `retentionObjectDays` / `retentionAudioDays`). It is now the device's own
|
|
@@ -5708,7 +5708,7 @@ var NcAudioStillShelf = class {
|
|
|
5708
5708
|
}
|
|
5709
5709
|
async fetch(captureId, deviceId, atMs) {
|
|
5710
5710
|
try {
|
|
5711
|
-
const shot = await withTimeout$
|
|
5711
|
+
const shot = await withTimeout$3(this.deps.getSnapshot(deviceId), SNAPSHOT_TIMEOUT_MS);
|
|
5712
5712
|
if (shot === null || shot.base64.length === 0) {
|
|
5713
5713
|
this.reportMiss(deviceId, "the camera returned no snapshot");
|
|
5714
5714
|
return;
|
|
@@ -5774,7 +5774,7 @@ function audioStillMedia(data, timestamp) {
|
|
|
5774
5774
|
];
|
|
5775
5775
|
}
|
|
5776
5776
|
/** Reject if `promise` does not settle within `ms`. */
|
|
5777
|
-
function withTimeout$
|
|
5777
|
+
function withTimeout$3(promise, ms) {
|
|
5778
5778
|
return new Promise((resolve, reject) => {
|
|
5779
5779
|
const timer = setTimeout(() => {
|
|
5780
5780
|
reject(/* @__PURE__ */ new Error(`snapshot cap timed out after ${String(ms)}ms`));
|
|
@@ -30426,7 +30426,7 @@ var undecided = (candidate, reason) => ({
|
|
|
30426
30426
|
reason,
|
|
30427
30427
|
className: candidate.className
|
|
30428
30428
|
});
|
|
30429
|
-
function withTimeout$
|
|
30429
|
+
function withTimeout$2(promise, timeoutMs) {
|
|
30430
30430
|
return new Promise((resolve, reject) => {
|
|
30431
30431
|
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("confirmation-timeout")), timeoutMs);
|
|
30432
30432
|
promise.then((value) => {
|
|
@@ -30466,7 +30466,7 @@ async function confirmOne(candidate, config, deps, exhausted) {
|
|
|
30466
30466
|
const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
|
|
30467
30467
|
if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
|
|
30468
30468
|
try {
|
|
30469
|
-
return await withTimeout$
|
|
30469
|
+
return await withTimeout$2(runConfirmation(candidate, config, deps, exhausted), config.timeoutMs);
|
|
30470
30470
|
} catch {
|
|
30471
30471
|
return undecided(candidate, "timeout");
|
|
30472
30472
|
}
|
|
@@ -34713,7 +34713,8 @@ function isMediaOwnerKind(value) {
|
|
|
34713
34713
|
* write="one row per blob written through put / putReplacing — a track's
|
|
34714
34714
|
* keyFrame, thumbnail, firstFrame and lastFrame (UPSERTED on a deterministic
|
|
34715
34715
|
* id, so one per track+kind however many captures race), its periodic
|
|
34716
|
-
* snapshots, each event's crop and full frames,
|
|
34716
|
+
* snapshots, each event's crop and full frames, a motion visit's still
|
|
34717
|
+
* (`kind: snapshot` on ownerKind event), and the buffered face/plate
|
|
34717
34718
|
* crops. A row is also REOWNED here (not rewritten) when the operator enrols a
|
|
34718
34719
|
* face or a plate, which is what moves it out of retention's reach."
|
|
34719
34720
|
* retention="no age sweep of its own — MediaStore.evictBefore was deleted (D59)
|
|
@@ -39019,11 +39020,14 @@ function tieredLabelColumnData(patch) {
|
|
|
39019
39020
|
* (MOTION_EVENT_HEARTBEAT_MS) while it stays on. The analyzer path and the
|
|
39020
39021
|
* onboard-firmware path share that throttle, so a camera cannot double-count.
|
|
39021
39022
|
* Measured at ~7,200 rows/day/camera."
|
|
39022
|
-
* retention="a TERMINAL age-keyed root — no trackId
|
|
39023
|
-
*
|
|
39023
|
+
* retention="a TERMINAL age-keyed root — no trackId. A motion row may own a
|
|
39024
|
+
* still (`ownerKind: 'event'`, `kind: 'snapshot'`) captured on the off→on
|
|
39025
|
+
* visit so events-mode recordings have a videoclip thumbnail. Deleted by
|
|
39024
39026
|
* EventStore.evictTracklessBefore on the DEVICE's one retention window
|
|
39025
|
-
* (follow-recordings horizon, or trackRetentionDays, default 7)
|
|
39026
|
-
*
|
|
39027
|
+
* (follow-recordings horizon, or trackRetentionDays, default 7), which
|
|
39028
|
+
* folds in MediaStore.deleteForEvents so the still leaves with the row.
|
|
39029
|
+
* 0 days = keep forever, and this table is the reason that setting is
|
|
39030
|
+
* dangerous."
|
|
39027
39031
|
*/
|
|
39028
39032
|
var MOTION_EVENTS_COLLECTION = "pipeline-analytics:motion-events";
|
|
39029
39033
|
/**
|
|
@@ -39752,9 +39756,10 @@ var EventStore = class {
|
|
|
39752
39756
|
* Object events now leave exactly one way: with their track, through
|
|
39753
39757
|
* {@link deleteByTracks} (D61).
|
|
39754
39758
|
*
|
|
39755
|
-
* Motion and audio carry no `trackId
|
|
39756
|
-
*
|
|
39757
|
-
*
|
|
39759
|
+
* Motion and audio carry no `trackId` and are referenced by nothing — they
|
|
39760
|
+
* are terminal retention ROOTS, so an age window on them cannot contradict
|
|
39761
|
+
* anybody's (`retention/retention-model.ts`). A motion visit still is
|
|
39762
|
+
* event-owned media of that row and is deleted here in lockstep.
|
|
39758
39763
|
*
|
|
39759
39764
|
* **ONE cutoff for both kinds** (D62). They used to take a window each
|
|
39760
39765
|
* (`retentionMotionDays` / `retentionAudioDays`), which is two clocks over
|
|
@@ -39771,9 +39776,17 @@ var EventStore = class {
|
|
|
39771
39776
|
...params.deviceId !== void 0 ? { where: { deviceId: params.deviceId } } : {},
|
|
39772
39777
|
whereBetween: { timestamp: [0, params.cutoffMs] }
|
|
39773
39778
|
});
|
|
39779
|
+
const motion = await batch(MOTION_EVENTS_COLLECTION);
|
|
39780
|
+
const audio = await batch(AUDIO_EVENTS_COLLECTION);
|
|
39781
|
+
const ids = [...motion, ...audio];
|
|
39782
|
+
if (ids.length > 0 && this.media !== void 0) try {
|
|
39783
|
+
await this.media.deleteForEvents(ids);
|
|
39784
|
+
} catch (err) {
|
|
39785
|
+
this.logger.warn("EventStore.evictTracklessBefore: event media delete failed", { meta: { error: String(err) } });
|
|
39786
|
+
}
|
|
39774
39787
|
return {
|
|
39775
|
-
motion
|
|
39776
|
-
audio
|
|
39788
|
+
motion,
|
|
39789
|
+
audio
|
|
39777
39790
|
};
|
|
39778
39791
|
}
|
|
39779
39792
|
/**
|
|
@@ -39798,23 +39811,32 @@ var EventStore = class {
|
|
|
39798
39811
|
* The SUBSET of `eventIds` that still exist — the orphan audit's liveness
|
|
39799
39812
|
* probe for event-owned media.
|
|
39800
39813
|
*
|
|
39814
|
+
* Motion and audio rows can own stills (`kind: 'snapshot'` on a motion
|
|
39815
|
+
* visit); probing only the object collection would classify those stills
|
|
39816
|
+
* as orphans the moment they were written.
|
|
39817
|
+
*
|
|
39801
39818
|
* THROWS on a query failure rather than answering partially. A probe that
|
|
39802
39819
|
* quietly under-reports here reads as "these events are gone", and the caller
|
|
39803
39820
|
* deletes live crops on the strength of it.
|
|
39804
39821
|
*/
|
|
39805
39822
|
async existingEventIds(eventIds) {
|
|
39806
39823
|
const found = /* @__PURE__ */ new Set();
|
|
39824
|
+
const collections = [
|
|
39825
|
+
OBJECT_EVENTS_COLLECTION,
|
|
39826
|
+
MOTION_EVENTS_COLLECTION,
|
|
39827
|
+
AUDIO_EVENTS_COLLECTION
|
|
39828
|
+
];
|
|
39807
39829
|
for (let i = 0; i < eventIds.length; i += 500) {
|
|
39808
39830
|
const chunk = eventIds.slice(i, i + 500);
|
|
39809
39831
|
if (chunk.length === 0) continue;
|
|
39810
|
-
const
|
|
39811
|
-
collection
|
|
39832
|
+
const pages = await Promise.all(collections.map((collection) => this.store.query.query({
|
|
39833
|
+
collection,
|
|
39812
39834
|
filter: {
|
|
39813
39835
|
whereIn: { id: [...chunk] },
|
|
39814
39836
|
limit: chunk.length
|
|
39815
39837
|
}
|
|
39816
|
-
});
|
|
39817
|
-
for (const row of rows) found.add(row.id);
|
|
39838
|
+
})));
|
|
39839
|
+
for (const rows of pages) for (const row of rows) found.add(row.id);
|
|
39818
39840
|
}
|
|
39819
39841
|
return found;
|
|
39820
39842
|
}
|
|
@@ -46672,16 +46694,16 @@ var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
|
46672
46694
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
46673
46695
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
46674
46696
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
46675
|
-
var SnapshotTimeoutError = class extends Error {
|
|
46697
|
+
var SnapshotTimeoutError$1 = class extends Error {
|
|
46676
46698
|
constructor(ms) {
|
|
46677
46699
|
super(`snapshot cap timed out after ${ms}ms`);
|
|
46678
46700
|
this.name = "SnapshotTimeoutError";
|
|
46679
46701
|
}
|
|
46680
46702
|
};
|
|
46681
46703
|
/** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
|
|
46682
|
-
function withTimeout(promise, ms) {
|
|
46704
|
+
function withTimeout$1(promise, ms) {
|
|
46683
46705
|
return new Promise((resolve, reject) => {
|
|
46684
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
46706
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError$1(ms)), ms);
|
|
46685
46707
|
promise.then((value) => {
|
|
46686
46708
|
clearTimeout(timer);
|
|
46687
46709
|
resolve(value);
|
|
@@ -46736,11 +46758,11 @@ var SyntheticTrackMaterializer = class {
|
|
|
46736
46758
|
*/
|
|
46737
46759
|
async fetchSnapshot(cameraId, timeoutMs) {
|
|
46738
46760
|
try {
|
|
46739
|
-
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
46761
|
+
const snap = await withTimeout$1(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
46740
46762
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"), cameraId);
|
|
46741
46763
|
return snap;
|
|
46742
46764
|
} catch (err) {
|
|
46743
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
46765
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError$1 ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
46744
46766
|
return null;
|
|
46745
46767
|
}
|
|
46746
46768
|
}
|
|
@@ -46806,6 +46828,66 @@ var SyntheticTrackMaterializer = class {
|
|
|
46806
46828
|
return track;
|
|
46807
46829
|
}
|
|
46808
46830
|
};
|
|
46831
|
+
var SnapshotTimeoutError = class extends Error {
|
|
46832
|
+
constructor(ms) {
|
|
46833
|
+
super(`snapshot cap timed out after ${ms}ms`);
|
|
46834
|
+
this.name = "SnapshotTimeoutError";
|
|
46835
|
+
}
|
|
46836
|
+
};
|
|
46837
|
+
function withTimeout(promise, ms) {
|
|
46838
|
+
return new Promise((resolve, reject) => {
|
|
46839
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
46840
|
+
promise.then((value) => {
|
|
46841
|
+
clearTimeout(timer);
|
|
46842
|
+
resolve(value);
|
|
46843
|
+
}, (err) => {
|
|
46844
|
+
clearTimeout(timer);
|
|
46845
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
46846
|
+
});
|
|
46847
|
+
});
|
|
46848
|
+
}
|
|
46849
|
+
var MotionEventSnapshotCapturer = class {
|
|
46850
|
+
deps;
|
|
46851
|
+
snapshotTimeoutMs;
|
|
46852
|
+
constructor(deps) {
|
|
46853
|
+
this.deps = deps;
|
|
46854
|
+
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
46855
|
+
}
|
|
46856
|
+
/**
|
|
46857
|
+
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
46858
|
+
* throws — the durable record is the motion row, already inserted by
|
|
46859
|
+
* the caller.
|
|
46860
|
+
*/
|
|
46861
|
+
async capture(input) {
|
|
46862
|
+
let snap;
|
|
46863
|
+
try {
|
|
46864
|
+
snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
46865
|
+
} catch (err) {
|
|
46866
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
46867
|
+
return;
|
|
46868
|
+
}
|
|
46869
|
+
if (snap === null) return;
|
|
46870
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
46871
|
+
let data = raw;
|
|
46872
|
+
try {
|
|
46873
|
+
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
46874
|
+
} catch (err) {
|
|
46875
|
+
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
46876
|
+
}
|
|
46877
|
+
try {
|
|
46878
|
+
await this.deps.media.put({
|
|
46879
|
+
deviceId: input.deviceId,
|
|
46880
|
+
ownerKind: "event",
|
|
46881
|
+
ownerId: input.eventId,
|
|
46882
|
+
kind: "snapshot",
|
|
46883
|
+
timestamp: input.timestamp,
|
|
46884
|
+
data
|
|
46885
|
+
});
|
|
46886
|
+
} catch (err) {
|
|
46887
|
+
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
46888
|
+
}
|
|
46889
|
+
}
|
|
46890
|
+
};
|
|
46809
46891
|
var TrackingSettingsSchema = object({
|
|
46810
46892
|
/**
|
|
46811
46893
|
* How much of a detection's box must lie inside a zone (0-1 fraction of the
|
|
@@ -49776,6 +49858,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
49776
49858
|
/** The ONE projector of non-spatial events into synthetic snapshot-carrying
|
|
49777
49859
|
* tracks — linked sensor/control state changes AND audio markers. */
|
|
49778
49860
|
syntheticTracks = null;
|
|
49861
|
+
/** Still for a motion row that has no track (events-mode recording without
|
|
49862
|
+
* a detection). Lands as event-owned `kind: 'snapshot'` so videoclip
|
|
49863
|
+
* thumbnails resolve through the existing event-media plane. */
|
|
49864
|
+
motionEventSnapshots = null;
|
|
49779
49865
|
/** Decides which audio frames deserve a synthetic marker (D62). */
|
|
49780
49866
|
audioMarkers = null;
|
|
49781
49867
|
faceStore = null;
|
|
@@ -50969,6 +51055,17 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
50969
51055
|
} })
|
|
50970
51056
|
});
|
|
50971
51057
|
this.syntheticTracks = syntheticTracks;
|
|
51058
|
+
this.motionEventSnapshots = new MotionEventSnapshotCapturer({
|
|
51059
|
+
snapshot: { getSnapshot: (i) => api.snapshot?.getSnapshot.query(i) ?? Promise.resolve(null) },
|
|
51060
|
+
media: mediaStore,
|
|
51061
|
+
onError: (scope, err, deviceId) => logger.warn("motion event snapshot failed", {
|
|
51062
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
51063
|
+
meta: {
|
|
51064
|
+
scope,
|
|
51065
|
+
error: errMsg(err)
|
|
51066
|
+
}
|
|
51067
|
+
})
|
|
51068
|
+
});
|
|
50972
51069
|
this.audioMarkers = new AudioMarkerProjector({
|
|
50973
51070
|
sink: syntheticTracks,
|
|
50974
51071
|
isMotionActive: (deviceId, atMs, quietMs) => this.isMotionActive(deviceId, atMs, quietMs),
|
|
@@ -54039,7 +54136,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
54039
54136
|
const last = this.lastMotionInsertByDevice.get(deviceId);
|
|
54040
54137
|
const heartbeatDue = !last || timestamp - last.atMs >= MOTION_EVENT_HEARTBEAT_MS;
|
|
54041
54138
|
const transitionedOn = detected && (last === void 0 || last.detected === false);
|
|
54042
|
-
if (!detected)
|
|
54139
|
+
if (!detected) {
|
|
54140
|
+
if (last?.detected === true) this.lastMotionInsertByDevice.set(deviceId, {
|
|
54141
|
+
detected: false,
|
|
54142
|
+
atMs: timestamp
|
|
54143
|
+
});
|
|
54144
|
+
return;
|
|
54145
|
+
}
|
|
54043
54146
|
if (!transitionedOn && !heartbeatDue) return;
|
|
54044
54147
|
const ev = {
|
|
54045
54148
|
id: randomUUID(),
|
|
@@ -54082,6 +54185,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
54082
54185
|
timestamp: ev.timestamp
|
|
54083
54186
|
}
|
|
54084
54187
|
});
|
|
54188
|
+
if (transitionedOn) await this.motionEventSnapshots?.capture({
|
|
54189
|
+
deviceId,
|
|
54190
|
+
eventId: ev.id,
|
|
54191
|
+
timestamp: ev.timestamp
|
|
54192
|
+
});
|
|
54085
54193
|
}
|
|
54086
54194
|
/**
|
|
54087
54195
|
* Persist firmware-driven (onboard) motion events. Mirrors
|
|
@@ -54106,7 +54214,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
54106
54214
|
const last = this.lastMotionInsertByDevice.get(deviceId);
|
|
54107
54215
|
const heartbeatDue = !last || timestamp - last.atMs >= MOTION_EVENT_HEARTBEAT_MS;
|
|
54108
54216
|
const transitionedOn = detected && (last === void 0 || last.detected === false);
|
|
54109
|
-
if (!detected)
|
|
54217
|
+
if (!detected) {
|
|
54218
|
+
if (last?.detected === true) this.lastMotionInsertByDevice.set(deviceId, {
|
|
54219
|
+
detected: false,
|
|
54220
|
+
atMs: timestamp
|
|
54221
|
+
});
|
|
54222
|
+
return;
|
|
54223
|
+
}
|
|
54110
54224
|
if (!transitionedOn && !heartbeatDue) return;
|
|
54111
54225
|
const regions = payload.regions ?? [];
|
|
54112
54226
|
const ev = {
|
|
@@ -54150,6 +54264,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
54150
54264
|
timestamp: ev.timestamp
|
|
54151
54265
|
}
|
|
54152
54266
|
});
|
|
54267
|
+
if (transitionedOn) await this.motionEventSnapshots?.capture({
|
|
54268
|
+
deviceId,
|
|
54269
|
+
eventId: ev.id,
|
|
54270
|
+
timestamp: ev.timestamp
|
|
54271
|
+
});
|
|
54153
54272
|
}
|
|
54154
54273
|
async sweepExpiredTracks() {
|
|
54155
54274
|
if (this.shuttingDown || !this.trackStore) return;
|
|
@@ -54218,8 +54337,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
54218
54337
|
*
|
|
54219
54338
|
* 1. the track sweep, per device, cascading;
|
|
54220
54339
|
* 2. the TRACKLESS roots — motion, audio and sensor events, which carry no
|
|
54221
|
-
* trackId
|
|
54222
|
-
*
|
|
54340
|
+
* trackId and are referenced by nothing, so the cascade can never reach
|
|
54341
|
+
* them and a clock is the only thing that can. Motion visit stills leave
|
|
54342
|
+
* with their row inside `evictTracklessBefore`.
|
|
54223
54343
|
*
|
|
54224
54344
|
* That clock used to be three more settings (`retentionMotionDays` /
|
|
54225
54345
|
* `retentionObjectDays` / `retentionAudioDays`). It is now the device's own
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.86",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|