@camstack/addon-post-analysis 1.1.32 → 1.1.34
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-CyyCe4TK.mjs → dist-B1EgWJrr.mjs} +361 -6
- package/dist/{dist-DU_JRm-j.js → dist-BeDNiKi6.js} +372 -5
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-K1C_KC3d.js → node-DYa6O693.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-aNR1K97R.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BuAr2V3O.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D6C5R_2c.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C8Wc_rxx.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-B3eluJsX.mjs → hostInit-DpLNAYiI.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +724 -10
- package/dist/pipeline-analytics/index.mjs +723 -9
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as object, S as number, T as EventCategory, _ as DeviceType, a as audioMetricsCapability, b as array, c as faceGalleryCapability, d as pipelineAnalyticsCapability, f as plateGalleryCapability, g as BaseAddon, h as errMsg, i as addonWidgetsSourceCapability, m as zoneAnalyticsCapability, n as EVENT_PAD_MS, o as cosineSimilarity, p as videoclipsCapability, r as OpsLogEntrySchema, t as EVENT_KIND_BY_CAP, u as nodePin, v as createEvent, w as string, x as boolean, y as hydrateSchema } from "../dist-B1EgWJrr.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -4393,6 +4393,48 @@ var MediaStore = class {
|
|
|
4393
4393
|
}
|
|
4394
4394
|
return removed;
|
|
4395
4395
|
}
|
|
4396
|
+
/**
|
|
4397
|
+
* On-demand footprint aggregation for the events-management UI: sum
|
|
4398
|
+
* `sizeBytes` and count rows per device across the whole media collection.
|
|
4399
|
+
* Pages by `id` (offset-based) so a large collection never loads at once.
|
|
4400
|
+
* Metadata rows are small (no blob) — the image bytes live on the storage
|
|
4401
|
+
* provider and are counted via the persisted `sizeBytes` column.
|
|
4402
|
+
*/
|
|
4403
|
+
async footprintByDevice() {
|
|
4404
|
+
const PAGE = 1e3;
|
|
4405
|
+
const out = /* @__PURE__ */ new Map();
|
|
4406
|
+
let offset = 0;
|
|
4407
|
+
for (;;) {
|
|
4408
|
+
const rows = await this.store.query.query({
|
|
4409
|
+
collection: MEDIA_COLLECTION,
|
|
4410
|
+
filter: {
|
|
4411
|
+
orderBy: {
|
|
4412
|
+
field: "id",
|
|
4413
|
+
direction: "asc"
|
|
4414
|
+
},
|
|
4415
|
+
limit: PAGE,
|
|
4416
|
+
offset
|
|
4417
|
+
}
|
|
4418
|
+
});
|
|
4419
|
+
if (rows.length === 0) break;
|
|
4420
|
+
for (const row of rows) {
|
|
4421
|
+
const data = row.data;
|
|
4422
|
+
const deviceId = Number(data["deviceId"]);
|
|
4423
|
+
const sizeBytes = Number(data["sizeBytes"]);
|
|
4424
|
+
if (!Number.isFinite(deviceId)) continue;
|
|
4425
|
+
const acc = out.get(deviceId) ?? {
|
|
4426
|
+
bytes: 0,
|
|
4427
|
+
rows: 0
|
|
4428
|
+
};
|
|
4429
|
+
acc.bytes += Number.isFinite(sizeBytes) ? sizeBytes : 0;
|
|
4430
|
+
acc.rows += 1;
|
|
4431
|
+
out.set(deviceId, acc);
|
|
4432
|
+
}
|
|
4433
|
+
if (rows.length < PAGE) break;
|
|
4434
|
+
offset += PAGE;
|
|
4435
|
+
}
|
|
4436
|
+
return out;
|
|
4437
|
+
}
|
|
4396
4438
|
/** Move a single media entry from one owner to another.
|
|
4397
4439
|
* The new copy is written before the old one is removed.
|
|
4398
4440
|
* The old-entry deletion is best-effort: on failure a warning is logged
|
|
@@ -4754,12 +4796,12 @@ var EventStore = class {
|
|
|
4754
4796
|
collection: MOTION_EVENTS_COLLECTION,
|
|
4755
4797
|
filter: this.buildFilter(q)
|
|
4756
4798
|
});
|
|
4757
|
-
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls(r.data)));
|
|
4799
|
+
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls$1(r.data)));
|
|
4758
4800
|
return rows.map((r) => {
|
|
4759
4801
|
return {
|
|
4760
4802
|
id: r.id,
|
|
4761
4803
|
kind: "motion",
|
|
4762
|
-
...stripNulls(r.data)
|
|
4804
|
+
...stripNulls$1(r.data)
|
|
4763
4805
|
};
|
|
4764
4806
|
});
|
|
4765
4807
|
}
|
|
@@ -4773,12 +4815,12 @@ var EventStore = class {
|
|
|
4773
4815
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
4774
4816
|
filter
|
|
4775
4817
|
});
|
|
4776
|
-
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls(r.data)));
|
|
4818
|
+
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls$1(r.data)));
|
|
4777
4819
|
return rows.map((r) => {
|
|
4778
4820
|
return {
|
|
4779
4821
|
id: r.id,
|
|
4780
4822
|
kind: "object",
|
|
4781
|
-
...stripNulls(r.data)
|
|
4823
|
+
...stripNulls$1(r.data)
|
|
4782
4824
|
};
|
|
4783
4825
|
});
|
|
4784
4826
|
}
|
|
@@ -4787,12 +4829,12 @@ var EventStore = class {
|
|
|
4787
4829
|
collection: AUDIO_EVENTS_COLLECTION,
|
|
4788
4830
|
filter: this.buildFilter(q)
|
|
4789
4831
|
});
|
|
4790
|
-
if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls(r.data)));
|
|
4832
|
+
if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls$1(r.data)));
|
|
4791
4833
|
return rows.map((r) => {
|
|
4792
4834
|
return {
|
|
4793
4835
|
id: r.id,
|
|
4794
4836
|
kind: "audio",
|
|
4795
|
-
...stripNulls(r.data)
|
|
4837
|
+
...stripNulls$1(r.data)
|
|
4796
4838
|
};
|
|
4797
4839
|
});
|
|
4798
4840
|
}
|
|
@@ -4938,6 +4980,23 @@ var EventStore = class {
|
|
|
4938
4980
|
return updated;
|
|
4939
4981
|
}
|
|
4940
4982
|
/**
|
|
4983
|
+
* Total persisted event rows (motion + object + audio) for a device — the
|
|
4984
|
+
* "rows" figure in the events-management footprint. Uses the indexed `count`
|
|
4985
|
+
* aggregate per collection, run in parallel.
|
|
4986
|
+
*/
|
|
4987
|
+
async countForDevice(deviceId) {
|
|
4988
|
+
const one = (collection) => this.store.count.query({
|
|
4989
|
+
collection,
|
|
4990
|
+
filter: { where: { deviceId } }
|
|
4991
|
+
});
|
|
4992
|
+
const [motion, object, audio] = await Promise.all([
|
|
4993
|
+
one(MOTION_EVENTS_COLLECTION),
|
|
4994
|
+
one(OBJECT_EVENTS_COLLECTION),
|
|
4995
|
+
one(AUDIO_EVENTS_COLLECTION)
|
|
4996
|
+
]);
|
|
4997
|
+
return motion + object + audio;
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
4941
5000
|
* Return per-kind event counts in equal-width time buckets.
|
|
4942
5001
|
*
|
|
4943
5002
|
* Each bucket maps to a `bucketStart = since + i * bucketMs`.
|
|
@@ -5212,12 +5271,189 @@ function bboxAreaFrac(data) {
|
|
|
5212
5271
|
if (w <= 0 || h <= 0) return 0;
|
|
5213
5272
|
return w * h / (fw * fh);
|
|
5214
5273
|
}
|
|
5215
|
-
function stripNulls(data) {
|
|
5274
|
+
function stripNulls$1(data) {
|
|
5216
5275
|
const out = {};
|
|
5217
5276
|
for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
|
|
5218
5277
|
return out;
|
|
5219
5278
|
}
|
|
5220
5279
|
//#endregion
|
|
5280
|
+
//#region src/pipeline-analytics/store/ops-log-store.ts
|
|
5281
|
+
/**
|
|
5282
|
+
* OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
|
|
5283
|
+
*
|
|
5284
|
+
* pipeline-analytics already owns SQLite collections, so (unlike the recorder's
|
|
5285
|
+
* DurableState ring) the events ops-log is a DECLARED SQL-backed collection.
|
|
5286
|
+
* MUST be declared in `onInitialize` via `declare()` before the first insert —
|
|
5287
|
+
* an undeclared collection crash-loops the runner.
|
|
5288
|
+
*
|
|
5289
|
+
* Collection name: pipeline-analytics:ops-log
|
|
5290
|
+
*
|
|
5291
|
+
* Append is BEST-EFFORT (`append` catches + logs, never throws) — a failed
|
|
5292
|
+
* audit write must not fail the operation it records.
|
|
5293
|
+
*/
|
|
5294
|
+
var OPS_LOG_COLLECTION = "pipeline-analytics:ops-log";
|
|
5295
|
+
var OPS_LOG_COLUMNS = [
|
|
5296
|
+
{
|
|
5297
|
+
name: "id",
|
|
5298
|
+
type: "TEXT",
|
|
5299
|
+
primaryKey: true,
|
|
5300
|
+
notNull: true
|
|
5301
|
+
},
|
|
5302
|
+
{
|
|
5303
|
+
name: "at",
|
|
5304
|
+
type: "INTEGER",
|
|
5305
|
+
notNull: true
|
|
5306
|
+
},
|
|
5307
|
+
{
|
|
5308
|
+
name: "domain",
|
|
5309
|
+
type: "TEXT",
|
|
5310
|
+
notNull: true
|
|
5311
|
+
},
|
|
5312
|
+
{
|
|
5313
|
+
name: "op",
|
|
5314
|
+
type: "TEXT",
|
|
5315
|
+
notNull: true
|
|
5316
|
+
},
|
|
5317
|
+
{
|
|
5318
|
+
name: "reason",
|
|
5319
|
+
type: "TEXT",
|
|
5320
|
+
notNull: true
|
|
5321
|
+
},
|
|
5322
|
+
{
|
|
5323
|
+
name: "deviceId",
|
|
5324
|
+
type: "INTEGER"
|
|
5325
|
+
},
|
|
5326
|
+
{
|
|
5327
|
+
name: "nodeId",
|
|
5328
|
+
type: "TEXT",
|
|
5329
|
+
notNull: true
|
|
5330
|
+
},
|
|
5331
|
+
{
|
|
5332
|
+
name: "itemsAffected",
|
|
5333
|
+
type: "INTEGER",
|
|
5334
|
+
notNull: true
|
|
5335
|
+
},
|
|
5336
|
+
{
|
|
5337
|
+
name: "bytesReclaimed",
|
|
5338
|
+
type: "INTEGER",
|
|
5339
|
+
notNull: true
|
|
5340
|
+
},
|
|
5341
|
+
{
|
|
5342
|
+
name: "detail",
|
|
5343
|
+
type: "TEXT"
|
|
5344
|
+
},
|
|
5345
|
+
{
|
|
5346
|
+
name: "actor",
|
|
5347
|
+
type: "TEXT",
|
|
5348
|
+
notNull: true
|
|
5349
|
+
}
|
|
5350
|
+
];
|
|
5351
|
+
var OPS_LOG_INDEXES = [{
|
|
5352
|
+
name: "idx_opslog_at",
|
|
5353
|
+
columns: ["at"]
|
|
5354
|
+
}, {
|
|
5355
|
+
name: "idx_opslog_device_at",
|
|
5356
|
+
columns: ["deviceId", "at"]
|
|
5357
|
+
}];
|
|
5358
|
+
var OpsLogStore = class {
|
|
5359
|
+
store;
|
|
5360
|
+
logger;
|
|
5361
|
+
nodeId;
|
|
5362
|
+
now;
|
|
5363
|
+
newId;
|
|
5364
|
+
constructor(deps) {
|
|
5365
|
+
this.store = deps.store;
|
|
5366
|
+
this.logger = deps.logger;
|
|
5367
|
+
this.nodeId = deps.nodeId;
|
|
5368
|
+
this.now = deps.now ?? (() => Date.now());
|
|
5369
|
+
this.newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
|
|
5370
|
+
}
|
|
5371
|
+
static async declare(store) {
|
|
5372
|
+
await store.declareCollection.mutate({
|
|
5373
|
+
collection: OPS_LOG_COLLECTION,
|
|
5374
|
+
columns: [...OPS_LOG_COLUMNS],
|
|
5375
|
+
indexes: [...OPS_LOG_INDEXES]
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
/**
|
|
5379
|
+
* Append one events-domain ops-log row. Best-effort — stamps
|
|
5380
|
+
* `domain:'events'`, `nodeId`, `id`, `at`, validates, inserts, and swallows
|
|
5381
|
+
* (logs) any failure so it can never fail the operation being audited.
|
|
5382
|
+
*/
|
|
5383
|
+
async append(input) {
|
|
5384
|
+
const entry = {
|
|
5385
|
+
id: this.newId(),
|
|
5386
|
+
at: this.now(),
|
|
5387
|
+
domain: "events",
|
|
5388
|
+
op: input.op,
|
|
5389
|
+
reason: input.reason,
|
|
5390
|
+
deviceId: input.deviceId,
|
|
5391
|
+
nodeId: this.nodeId,
|
|
5392
|
+
itemsAffected: input.itemsAffected,
|
|
5393
|
+
bytesReclaimed: input.bytesReclaimed,
|
|
5394
|
+
detail: input.detail ?? null,
|
|
5395
|
+
actor: input.actor ?? "operator"
|
|
5396
|
+
};
|
|
5397
|
+
try {
|
|
5398
|
+
const { id, ...rest } = OpsLogEntrySchema.parse(entry);
|
|
5399
|
+
await this.store.insert.mutate({
|
|
5400
|
+
collection: OPS_LOG_COLLECTION,
|
|
5401
|
+
record: {
|
|
5402
|
+
id,
|
|
5403
|
+
data: rest
|
|
5404
|
+
}
|
|
5405
|
+
});
|
|
5406
|
+
} catch (err) {
|
|
5407
|
+
this.logger.warn("OpsLogStore.append failed (best-effort)", {
|
|
5408
|
+
tags: { deviceId: input.deviceId ?? void 0 },
|
|
5409
|
+
meta: {
|
|
5410
|
+
op: input.op,
|
|
5411
|
+
error: String(err)
|
|
5412
|
+
}
|
|
5413
|
+
});
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
/**
|
|
5417
|
+
* List events ops-log rows newest-first, optionally scoped to one device,
|
|
5418
|
+
* capped at `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
5419
|
+
*/
|
|
5420
|
+
async list(query) {
|
|
5421
|
+
const filter = {
|
|
5422
|
+
orderBy: {
|
|
5423
|
+
field: "at",
|
|
5424
|
+
direction: "desc"
|
|
5425
|
+
},
|
|
5426
|
+
limit: query.limit ?? 200
|
|
5427
|
+
};
|
|
5428
|
+
if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
|
|
5429
|
+
const rows = await this.store.query.query({
|
|
5430
|
+
collection: OPS_LOG_COLLECTION,
|
|
5431
|
+
filter
|
|
5432
|
+
});
|
|
5433
|
+
const out = [];
|
|
5434
|
+
for (const r of rows) {
|
|
5435
|
+
const parsed = OpsLogEntrySchema.safeParse({
|
|
5436
|
+
id: r.id,
|
|
5437
|
+
...stripNulls(r.data)
|
|
5438
|
+
});
|
|
5439
|
+
if (parsed.success) out.push(parsed.data);
|
|
5440
|
+
else this.logger.debug("OpsLogStore.list: skipped malformed row", { meta: { id: r.id } });
|
|
5441
|
+
}
|
|
5442
|
+
return out;
|
|
5443
|
+
}
|
|
5444
|
+
};
|
|
5445
|
+
/** SQLite stores nullable columns as `null`; the row schema uses `.nullable()`
|
|
5446
|
+
* for deviceId/detail (accepts null) but drop any stray null on required
|
|
5447
|
+
* fields defensively before parse. */
|
|
5448
|
+
function stripNulls(data) {
|
|
5449
|
+
const out = {};
|
|
5450
|
+
for (const [k, v] of Object.entries(data)) {
|
|
5451
|
+
if (v === null && k !== "deviceId" && k !== "detail") continue;
|
|
5452
|
+
out[k] = v;
|
|
5453
|
+
}
|
|
5454
|
+
return out;
|
|
5455
|
+
}
|
|
5456
|
+
//#endregion
|
|
5221
5457
|
//#region src/pipeline-analytics/store/sensor-event-store.ts
|
|
5222
5458
|
var SENSOR_EVENTS_COLLECTION = "pipeline-analytics:sensor-events";
|
|
5223
5459
|
var SENSOR_EVENT_COLUMNS = [
|
|
@@ -5396,6 +5632,7 @@ var PERSON_COLOR = "#22c55e";
|
|
|
5396
5632
|
var VEHICLE_COLOR = "#3b82f6";
|
|
5397
5633
|
var ANIMAL_COLOR = "#f97316";
|
|
5398
5634
|
var GENERIC_DETECTION_COLOR = "#64748b";
|
|
5635
|
+
var PACKAGE_COLOR = "#a855f7";
|
|
5399
5636
|
var VEHICLE_CLASSES = new Set([
|
|
5400
5637
|
"vehicle",
|
|
5401
5638
|
"car",
|
|
@@ -5473,6 +5710,34 @@ async function composeEventKinds(deps, deviceId) {
|
|
|
5473
5710
|
} catch (err) {
|
|
5474
5711
|
deps.onError?.("observedClassNames", err);
|
|
5475
5712
|
}
|
|
5713
|
+
try {
|
|
5714
|
+
if (deps.packageZonesEnabled && await deps.packageZonesEnabled(deviceId)) {
|
|
5715
|
+
out.push({
|
|
5716
|
+
kind: "package-delivered",
|
|
5717
|
+
label: "Package delivered",
|
|
5718
|
+
color: PACKAGE_COLOR,
|
|
5719
|
+
icon: "package",
|
|
5720
|
+
category: "package",
|
|
5721
|
+
source: {
|
|
5722
|
+
capName: "pipeline-analytics",
|
|
5723
|
+
deviceId
|
|
5724
|
+
}
|
|
5725
|
+
});
|
|
5726
|
+
out.push({
|
|
5727
|
+
kind: "package-picked-up",
|
|
5728
|
+
label: "Package picked up",
|
|
5729
|
+
color: PACKAGE_COLOR,
|
|
5730
|
+
icon: "package",
|
|
5731
|
+
category: "package",
|
|
5732
|
+
source: {
|
|
5733
|
+
capName: "pipeline-analytics",
|
|
5734
|
+
deviceId
|
|
5735
|
+
}
|
|
5736
|
+
});
|
|
5737
|
+
}
|
|
5738
|
+
} catch (err) {
|
|
5739
|
+
deps.onError?.("packageZonesEnabled", err);
|
|
5740
|
+
}
|
|
5476
5741
|
try {
|
|
5477
5742
|
const { devices } = await deps.linkedDevices.getLinkedDevices({ deviceId });
|
|
5478
5743
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -7325,6 +7590,208 @@ function resolveMediaSettings(raw) {
|
|
|
7325
7590
|
snapshotMaxIdleMs: pick("snapshotMaxIdleMs")
|
|
7326
7591
|
};
|
|
7327
7592
|
}
|
|
7593
|
+
//#endregion
|
|
7594
|
+
//#region src/pipeline-analytics/package-settings.ts
|
|
7595
|
+
/**
|
|
7596
|
+
* Per-device package-drop detector settings (surface C of the
|
|
7597
|
+
* detection-config-exposure plan — a per-device post-analysis section).
|
|
7598
|
+
* Cascade: a per-device override on top of the declared default, resolved
|
|
7599
|
+
* per field (an invalid/missing value falls back to its default — parse
|
|
7600
|
+
* never throws). Mirrors `media-settings` / `tracking-settings`.
|
|
7601
|
+
*
|
|
7602
|
+
* Keys are namespaced (`packageDrop*`) because the device store is a FLAT
|
|
7603
|
+
* blob shared across every post-analysis section — a bare `enabled` would
|
|
7604
|
+
* collide with another section.
|
|
7605
|
+
*
|
|
7606
|
+
* See docs/superpowers/specs/2026-07-17-package-zones-design.md §3.3 +
|
|
7607
|
+
* docs/superpowers/specs/2026-07-17-detection-config-exposure-design.md
|
|
7608
|
+
* (surface C). Dwell default is 15s per operator decision (the design's
|
|
7609
|
+
* §3.3 draft said 45s).
|
|
7610
|
+
*/
|
|
7611
|
+
var PackageDropSettingsSchema = object({
|
|
7612
|
+
/** Master switch — off by default; opt-in per camera (porch/door cams). */
|
|
7613
|
+
packageDropEnabled: boolean().default(false),
|
|
7614
|
+
/**
|
|
7615
|
+
* Minimum OBSERVED dwell (seconds, since first-seen) before a newly
|
|
7616
|
+
* promoted stationary package counts as a delivery. Kills a bag briefly
|
|
7617
|
+
* set down and snatched back. Operator default 15s. Values beyond the
|
|
7618
|
+
* stationary promotion window (30s) are satisfied only once the entry
|
|
7619
|
+
* has actually been observed that long.
|
|
7620
|
+
*/
|
|
7621
|
+
packageDropDwellSec: number().int().min(0).max(3600).default(15),
|
|
7622
|
+
/** Reject tiny far-field blobs: min bbox area as a fraction of the frame. */
|
|
7623
|
+
packageDropMinBboxAreaFrac: number().min(0).max(1).default(.004),
|
|
7624
|
+
/** Emit `package-picked-up` when a delivered package's entry departs. */
|
|
7625
|
+
packageDropPickupEnabled: boolean().default(true),
|
|
7626
|
+
/**
|
|
7627
|
+
* Stationary classes that count as a package. MODEL-AGNOSTIC: defaults to
|
|
7628
|
+
* the single `package` class a dedicated parcel model emits; the interim
|
|
7629
|
+
* COCO vector (suitcase/backpack/handbag) already maps to `package`
|
|
7630
|
+
* upstream, so this stays `['package']`.
|
|
7631
|
+
*/
|
|
7632
|
+
packageDropClassFilter: array(string()).default(["package"])
|
|
7633
|
+
});
|
|
7634
|
+
var PACKAGE_DROP_DEFAULTS = PackageDropSettingsSchema.parse({});
|
|
7635
|
+
/**
|
|
7636
|
+
* Resolve a per-device store blob into typed package-drop settings.
|
|
7637
|
+
* Unknown/invalid fields fall back to the default for that field (never
|
|
7638
|
+
* throws on a bad blob).
|
|
7639
|
+
*/
|
|
7640
|
+
function resolvePackageDropSettings(raw) {
|
|
7641
|
+
const pick = (key) => {
|
|
7642
|
+
const parsed = PackageDropSettingsSchema.shape[key].safeParse(raw[key]);
|
|
7643
|
+
return parsed.success ? parsed.data : PACKAGE_DROP_DEFAULTS[key];
|
|
7644
|
+
};
|
|
7645
|
+
return {
|
|
7646
|
+
packageDropEnabled: pick("packageDropEnabled"),
|
|
7647
|
+
packageDropDwellSec: pick("packageDropDwellSec"),
|
|
7648
|
+
packageDropMinBboxAreaFrac: pick("packageDropMinBboxAreaFrac"),
|
|
7649
|
+
packageDropPickupEnabled: pick("packageDropPickupEnabled"),
|
|
7650
|
+
packageDropClassFilter: pick("packageDropClassFilter")
|
|
7651
|
+
};
|
|
7652
|
+
}
|
|
7653
|
+
//#endregion
|
|
7654
|
+
//#region src/pipeline-analytics/pipeline/package-drop-detector.ts
|
|
7655
|
+
/** The class every durable package event carries. */
|
|
7656
|
+
var PACKAGE_EVENT_CLASS = "package";
|
|
7657
|
+
/** Fixed high importance — package delivery is inherently high-signal (§6). */
|
|
7658
|
+
var PACKAGE_IMPORTANCE = 1;
|
|
7659
|
+
/** Object-event `state` used for a delivery (a parked package) / a pick-up
|
|
7660
|
+
* (its departure). Both are valid `TrackState` enum members so the events
|
|
7661
|
+
* stay `getObjectEvents`-output-valid. */
|
|
7662
|
+
var DELIVERED_STATE = "idle";
|
|
7663
|
+
var PICKED_UP_STATE = "left";
|
|
7664
|
+
/** Deterministic durable-event ids keyed on the stationary entry id. */
|
|
7665
|
+
function deliveredEventId(entryId) {
|
|
7666
|
+
return `pa-pkg-${entryId}-delivered`;
|
|
7667
|
+
}
|
|
7668
|
+
function pickedUpEventId(entryId) {
|
|
7669
|
+
return `pa-pkg-${entryId}-pickedup`;
|
|
7670
|
+
}
|
|
7671
|
+
var PackageDropDetector = class {
|
|
7672
|
+
deps;
|
|
7673
|
+
constructor(deps) {
|
|
7674
|
+
this.deps = deps;
|
|
7675
|
+
}
|
|
7676
|
+
/** Single entrypoint — bridge the registry's `onChange` here. Never
|
|
7677
|
+
* throws (telemetry-lossy, D8): a failure only misses one package event. */
|
|
7678
|
+
async onStationaryChange(change) {
|
|
7679
|
+
try {
|
|
7680
|
+
if (change.phase === "appeared") await this.onAppeared(change.entry, change.timestamp);
|
|
7681
|
+
else await this.onDeparted(change.entry, change.timestamp);
|
|
7682
|
+
} catch (err) {
|
|
7683
|
+
this.deps.onError?.("onStationaryChange", err);
|
|
7684
|
+
this.deps.logger.warn("package-drop detector failed on change", {
|
|
7685
|
+
tags: { deviceId: change.entry.deviceId },
|
|
7686
|
+
meta: {
|
|
7687
|
+
phase: change.phase,
|
|
7688
|
+
entryId: change.entry.id,
|
|
7689
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7690
|
+
}
|
|
7691
|
+
});
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
async onAppeared(entry, timestamp) {
|
|
7695
|
+
const settings = await this.deps.resolveSettings(entry.deviceId);
|
|
7696
|
+
if (!settings.packageDropEnabled) return;
|
|
7697
|
+
if (!settings.packageDropClassFilter.includes(entry.className)) return;
|
|
7698
|
+
const rules = (await this.deps.resolvePackageRules(entry.deviceId)).filter((r) => r.enabled !== false);
|
|
7699
|
+
if (rules.length === 0) return;
|
|
7700
|
+
const ruleZoneIds = new Set(rules.flatMap((r) => r.zoneIds));
|
|
7701
|
+
const hitZones = computeStationaryEntryZones(entry, await this.deps.resolveZones(entry.deviceId)).filter((z) => ruleZoneIds.has(z));
|
|
7702
|
+
if (hitZones.length === 0) return;
|
|
7703
|
+
if (timestamp - entry.firstSeenAt < settings.packageDropDwellSec * 1e3) return;
|
|
7704
|
+
const frameArea = entry.frameWidth * entry.frameHeight;
|
|
7705
|
+
if (frameArea <= 0) return;
|
|
7706
|
+
if (entry.bbox.w * entry.bbox.h / frameArea < settings.packageDropMinBboxAreaFrac) return;
|
|
7707
|
+
const eventId = deliveredEventId(entry.id);
|
|
7708
|
+
if ((await this.deps.events.queryObject({
|
|
7709
|
+
deviceId: entry.deviceId,
|
|
7710
|
+
classFilter: "package"
|
|
7711
|
+
})).some((e) => e.id === eventId)) return;
|
|
7712
|
+
const ev = {
|
|
7713
|
+
id: eventId,
|
|
7714
|
+
kind: "object",
|
|
7715
|
+
deviceId: entry.deviceId,
|
|
7716
|
+
timestamp,
|
|
7717
|
+
source: "pipeline",
|
|
7718
|
+
trackId: entry.sourceTrackId ?? entry.id,
|
|
7719
|
+
className: PACKAGE_EVENT_CLASS,
|
|
7720
|
+
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
7721
|
+
confidence: PACKAGE_IMPORTANCE,
|
|
7722
|
+
bbox: {
|
|
7723
|
+
x: entry.bbox.x,
|
|
7724
|
+
y: entry.bbox.y,
|
|
7725
|
+
w: entry.bbox.w,
|
|
7726
|
+
h: entry.bbox.h
|
|
7727
|
+
},
|
|
7728
|
+
zones: hitZones,
|
|
7729
|
+
state: DELIVERED_STATE,
|
|
7730
|
+
frameWidth: entry.frameWidth,
|
|
7731
|
+
frameHeight: entry.frameHeight,
|
|
7732
|
+
...entry.keyFrameMediaKey !== void 0 ? { mediaKey: entry.keyFrameMediaKey } : {},
|
|
7733
|
+
importance: PACKAGE_IMPORTANCE
|
|
7734
|
+
};
|
|
7735
|
+
await this.deps.events.insertObject(ev);
|
|
7736
|
+
this.deps.emit.delivered({
|
|
7737
|
+
deviceId: entry.deviceId,
|
|
7738
|
+
entryId: entry.id,
|
|
7739
|
+
eventId,
|
|
7740
|
+
className: entry.className,
|
|
7741
|
+
zoneIds: hitZones,
|
|
7742
|
+
...entry.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: entry.keyFrameMediaKey } : {},
|
|
7743
|
+
bbox: {
|
|
7744
|
+
x: entry.bbox.x,
|
|
7745
|
+
y: entry.bbox.y,
|
|
7746
|
+
w: entry.bbox.w,
|
|
7747
|
+
h: entry.bbox.h
|
|
7748
|
+
},
|
|
7749
|
+
timestamp
|
|
7750
|
+
});
|
|
7751
|
+
}
|
|
7752
|
+
async onDeparted(entry, timestamp) {
|
|
7753
|
+
if (!(await this.deps.resolveSettings(entry.deviceId)).packageDropPickupEnabled) return;
|
|
7754
|
+
const deliveredId = deliveredEventId(entry.id);
|
|
7755
|
+
const pickedUpId = pickedUpEventId(entry.id);
|
|
7756
|
+
const rows = await this.deps.events.queryObject({
|
|
7757
|
+
deviceId: entry.deviceId,
|
|
7758
|
+
classFilter: PACKAGE_EVENT_CLASS
|
|
7759
|
+
});
|
|
7760
|
+
const delivered = rows.find((e) => e.id === deliveredId);
|
|
7761
|
+
if (delivered === void 0) return;
|
|
7762
|
+
if (rows.some((e) => e.id === pickedUpId)) return;
|
|
7763
|
+
const ev = {
|
|
7764
|
+
id: pickedUpId,
|
|
7765
|
+
kind: "object",
|
|
7766
|
+
deviceId: entry.deviceId,
|
|
7767
|
+
timestamp,
|
|
7768
|
+
source: "pipeline",
|
|
7769
|
+
trackId: entry.sourceTrackId ?? entry.id,
|
|
7770
|
+
className: PACKAGE_EVENT_CLASS,
|
|
7771
|
+
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
7772
|
+
confidence: PACKAGE_IMPORTANCE,
|
|
7773
|
+
bbox: {
|
|
7774
|
+
x: entry.bbox.x,
|
|
7775
|
+
y: entry.bbox.y,
|
|
7776
|
+
w: entry.bbox.w,
|
|
7777
|
+
h: entry.bbox.h
|
|
7778
|
+
},
|
|
7779
|
+
...delivered.zones !== void 0 ? { zones: delivered.zones } : {},
|
|
7780
|
+
state: PICKED_UP_STATE,
|
|
7781
|
+
frameWidth: entry.frameWidth,
|
|
7782
|
+
frameHeight: entry.frameHeight,
|
|
7783
|
+
importance: PACKAGE_IMPORTANCE
|
|
7784
|
+
};
|
|
7785
|
+
await this.deps.events.insertObject(ev);
|
|
7786
|
+
this.deps.emit.pickedUp({
|
|
7787
|
+
deviceId: entry.deviceId,
|
|
7788
|
+
entryId: entry.id,
|
|
7789
|
+
deliveredEventId: deliveredId,
|
|
7790
|
+
className: entry.className,
|
|
7791
|
+
timestamp
|
|
7792
|
+
});
|
|
7793
|
+
}
|
|
7794
|
+
};
|
|
7328
7795
|
function centroidOf(b) {
|
|
7329
7796
|
return {
|
|
7330
7797
|
x: b.x + b.w / 2,
|
|
@@ -10728,6 +11195,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
10728
11195
|
stationaryRegistry = null;
|
|
10729
11196
|
mediaStore = null;
|
|
10730
11197
|
eventStore = null;
|
|
11198
|
+
/** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
|
|
11199
|
+
eventsOpsLog = null;
|
|
10731
11200
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
10732
11201
|
sensorEventStore = null;
|
|
10733
11202
|
/** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
|
|
@@ -10805,6 +11274,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
10805
11274
|
/** GLOBAL face-recognition master switch (addon store), TTL-cached. */
|
|
10806
11275
|
faceGlobalEnabledCache = null;
|
|
10807
11276
|
mediaCacheByDevice = /* @__PURE__ */ new Map();
|
|
11277
|
+
packageDropCacheByDevice = /* @__PURE__ */ new Map();
|
|
11278
|
+
/** Turns stationary appear/depart into package-delivered/picked-up events. */
|
|
11279
|
+
packageDropDetector = null;
|
|
10808
11280
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
10809
11281
|
dropoutSkipsByKey = /* @__PURE__ */ new Map();
|
|
10810
11282
|
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
@@ -10887,6 +11359,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
10887
11359
|
await VehicleStore.declare(api.settingsStore);
|
|
10888
11360
|
await ObjectEmbeddingStore.declare(api.settingsStore);
|
|
10889
11361
|
await StationaryObjectRegistry.declare(api.settingsStore);
|
|
11362
|
+
await OpsLogStore.declare(api.settingsStore);
|
|
10890
11363
|
const logger = this.ctx.logger;
|
|
10891
11364
|
let storage = this.ctx.kernel.storage;
|
|
10892
11365
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
@@ -10922,6 +11395,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
10922
11395
|
timestamp
|
|
10923
11396
|
}
|
|
10924
11397
|
});
|
|
11398
|
+
this.packageDropDetector?.onStationaryChange({
|
|
11399
|
+
phase,
|
|
11400
|
+
entry,
|
|
11401
|
+
timestamp
|
|
11402
|
+
});
|
|
10925
11403
|
}
|
|
10926
11404
|
});
|
|
10927
11405
|
await this.stationaryRegistry.load();
|
|
@@ -10930,15 +11408,55 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
10930
11408
|
store: api.settingsStore,
|
|
10931
11409
|
logger: logger.child("MediaStore")
|
|
10932
11410
|
});
|
|
10933
|
-
|
|
11411
|
+
const eventStore = new EventStore({
|
|
10934
11412
|
store: api.settingsStore,
|
|
10935
11413
|
logger: logger.child("EventStore"),
|
|
10936
11414
|
media: this.mediaStore
|
|
10937
11415
|
});
|
|
11416
|
+
this.eventStore = eventStore;
|
|
10938
11417
|
this.sensorEventStore = new SensorEventStore({
|
|
10939
11418
|
store: api.settingsStore,
|
|
10940
11419
|
logger: logger.child("SensorEventStore")
|
|
10941
11420
|
});
|
|
11421
|
+
this.packageDropDetector = new PackageDropDetector({
|
|
11422
|
+
events: eventStore,
|
|
11423
|
+
emit: {
|
|
11424
|
+
delivered: (payload) => {
|
|
11425
|
+
this.ctx.eventBus.emit({
|
|
11426
|
+
id: `pa-package-delivered-${payload.entryId}`,
|
|
11427
|
+
timestamp: new Date(payload.timestamp),
|
|
11428
|
+
source: {
|
|
11429
|
+
type: "addon",
|
|
11430
|
+
id: "pipeline-analytics",
|
|
11431
|
+
addonId: "pipeline-analytics"
|
|
11432
|
+
},
|
|
11433
|
+
category: EventCategory.PipelineAnalyticsPackageDelivered,
|
|
11434
|
+
data: { ...payload }
|
|
11435
|
+
});
|
|
11436
|
+
},
|
|
11437
|
+
pickedUp: (payload) => {
|
|
11438
|
+
this.ctx.eventBus.emit({
|
|
11439
|
+
id: `pa-package-pickedup-${payload.entryId}`,
|
|
11440
|
+
timestamp: new Date(payload.timestamp),
|
|
11441
|
+
source: {
|
|
11442
|
+
type: "addon",
|
|
11443
|
+
id: "pipeline-analytics",
|
|
11444
|
+
addonId: "pipeline-analytics"
|
|
11445
|
+
},
|
|
11446
|
+
category: EventCategory.PipelineAnalyticsPackagePickedUp,
|
|
11447
|
+
data: { ...payload }
|
|
11448
|
+
});
|
|
11449
|
+
}
|
|
11450
|
+
},
|
|
11451
|
+
logger: logger.child("PackageDropDetector"),
|
|
11452
|
+
resolveZones: (deviceId) => this.resolveDeviceZones(deviceId),
|
|
11453
|
+
resolvePackageRules: (deviceId) => this.resolveDevicePackageRules(deviceId),
|
|
11454
|
+
resolveSettings: (deviceId) => this.resolveDevicePackageDropSettings(deviceId),
|
|
11455
|
+
onError: (scope, err) => logger.warn("package-drop detector error", { meta: {
|
|
11456
|
+
scope,
|
|
11457
|
+
error: errMsg(err)
|
|
11458
|
+
} })
|
|
11459
|
+
});
|
|
10942
11460
|
this.linkedCamerasCache = new LinkedCamerasCache({
|
|
10943
11461
|
cameras: { listCameraIds: async () => {
|
|
10944
11462
|
return (await api.deviceManager.listAll.query({})).filter((d) => d.isCamera).map((d) => d.id);
|
|
@@ -10967,6 +11485,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
10967
11485
|
});
|
|
10968
11486
|
const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
10969
11487
|
const ownNodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
|
|
11488
|
+
this.eventsOpsLog = new OpsLogStore({
|
|
11489
|
+
store: api.settingsStore,
|
|
11490
|
+
logger: logger.child("ops-log"),
|
|
11491
|
+
nodeId: ownNodeId
|
|
11492
|
+
});
|
|
10970
11493
|
{
|
|
10971
11494
|
const designated = await this.postProcessingNodeState.get();
|
|
10972
11495
|
this.isPostProcessingNode = ownNodeId === designated;
|
|
@@ -11218,6 +11741,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11218
11741
|
this.trackingCacheByDevice.delete(data.deviceId);
|
|
11219
11742
|
this.faceCacheByDevice.delete(data.deviceId);
|
|
11220
11743
|
this.mediaCacheByDevice.delete(data.deviceId);
|
|
11744
|
+
this.packageDropCacheByDevice.delete(data.deviceId);
|
|
11221
11745
|
}
|
|
11222
11746
|
});
|
|
11223
11747
|
this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (ev) => {
|
|
@@ -11233,6 +11757,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11233
11757
|
this.trackingCacheByDevice.delete(deviceId);
|
|
11234
11758
|
this.faceCacheByDevice.delete(deviceId);
|
|
11235
11759
|
this.mediaCacheByDevice.delete(deviceId);
|
|
11760
|
+
this.packageDropCacheByDevice.delete(deviceId);
|
|
11236
11761
|
this.bindingCache?.invalidate(deviceId);
|
|
11237
11762
|
this.zoneAnalytics?.forgetDevice(deviceId);
|
|
11238
11763
|
this.audioMetrics?.forgetDevice(deviceId);
|
|
@@ -11580,6 +12105,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11580
12105
|
this.faceCacheByDevice.clear();
|
|
11581
12106
|
this.faceGlobalEnabledCache = null;
|
|
11582
12107
|
this.mediaCacheByDevice.clear();
|
|
12108
|
+
this.packageDropCacheByDevice.clear();
|
|
11583
12109
|
this.trackStore?.clearAll();
|
|
11584
12110
|
this.stationaryRegistry = null;
|
|
11585
12111
|
this.bindingCache?.clearAll();
|
|
@@ -12055,6 +12581,32 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12055
12581
|
});
|
|
12056
12582
|
return settings;
|
|
12057
12583
|
}
|
|
12584
|
+
async resolveDevicePackageDropSettings(deviceId) {
|
|
12585
|
+
const now = Date.now();
|
|
12586
|
+
const cached = this.packageDropCacheByDevice.get(deviceId);
|
|
12587
|
+
if (cached && now < cached.expiresAt) return cached.settings;
|
|
12588
|
+
const settings = resolvePackageDropSettings(await this.ctx?.settings?.readDeviceStore(deviceId) ?? {});
|
|
12589
|
+
this.packageDropCacheByDevice.set(deviceId, {
|
|
12590
|
+
settings,
|
|
12591
|
+
expiresAt: now + SETTINGS_CACHE_TTL_MS
|
|
12592
|
+
});
|
|
12593
|
+
return settings;
|
|
12594
|
+
}
|
|
12595
|
+
/**
|
|
12596
|
+
* Resolve a device's ENABLED `package`-stage zone rules independent of the
|
|
12597
|
+
* live frame path (mirrors `resolveDeviceZones`). Warms the proxy and,
|
|
12598
|
+
* when the cached slice is empty, forces one refresh. The `package` slice
|
|
12599
|
+
* is written by the orchestrator's package-stage provider (a later slice);
|
|
12600
|
+
* until then this returns `[]` and no package events fire.
|
|
12601
|
+
*/
|
|
12602
|
+
async resolveDevicePackageRules(deviceId) {
|
|
12603
|
+
const proxy = await this.ensureProxy(deviceId);
|
|
12604
|
+
if (!proxy) return [];
|
|
12605
|
+
const cached = proxy.state.zoneRules.value?.package;
|
|
12606
|
+
if (cached && cached.length > 0) return cached;
|
|
12607
|
+
await proxy.state.zoneRules.refresh().catch(() => void 0);
|
|
12608
|
+
return proxy.state.zoneRules.value?.package ?? [];
|
|
12609
|
+
}
|
|
12058
12610
|
/**
|
|
12059
12611
|
* Route one track's `runDetailSubtree` results (two-plane detail dispatch)
|
|
12060
12612
|
* into the EXISTING per-track consumers, discriminated by payload SHAPE:
|
|
@@ -13156,6 +13708,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13156
13708
|
linkedDevices: { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
|
|
13157
13709
|
bindings: { getBindings: (i) => api.deviceManager.getBindings.query(i) },
|
|
13158
13710
|
observedClassNames: async (deviceId) => this.trackStore?.observedClassNames(deviceId) ?? [],
|
|
13711
|
+
packageZonesEnabled: async (deviceId) => {
|
|
13712
|
+
return (await this.resolveDevicePackageRules(deviceId)).some((r) => r.enabled !== false);
|
|
13713
|
+
},
|
|
13159
13714
|
onError: (scope, err) => this.ctx.logger.warn("listEventKinds: partial compose", {
|
|
13160
13715
|
tags: { deviceId: input.deviceId },
|
|
13161
13716
|
meta: {
|
|
@@ -13440,6 +13995,120 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13440
13995
|
};
|
|
13441
13996
|
}
|
|
13442
13997
|
/**
|
|
13998
|
+
* Durable event-store footprint for the events-management UI: event ROWS
|
|
13999
|
+
* (motion + object + audio) counted per camera (indexed `count`) + total, and
|
|
14000
|
+
* event-owned media BYTES on disk per camera (paged `sizeBytes` sum) + total.
|
|
14001
|
+
* The device set is the union of cameras that have media OR persisted tracks;
|
|
14002
|
+
* cameras with neither rows nor bytes are omitted.
|
|
14003
|
+
*/
|
|
14004
|
+
async getEventStoreFootprint() {
|
|
14005
|
+
const eventStore = this.eventStore;
|
|
14006
|
+
const mediaStore = this.mediaStore;
|
|
14007
|
+
if (!eventStore || !mediaStore) return {
|
|
14008
|
+
totalRows: 0,
|
|
14009
|
+
totalBytes: 0,
|
|
14010
|
+
devices: []
|
|
14011
|
+
};
|
|
14012
|
+
const mediaByDevice = await mediaStore.footprintByDevice();
|
|
14013
|
+
const deviceIds = new Set(mediaByDevice.keys());
|
|
14014
|
+
if (this.trackStore) for (const id of await this.trackStore.listDeviceIds()) deviceIds.add(id);
|
|
14015
|
+
const devices = [];
|
|
14016
|
+
let totalRows = 0;
|
|
14017
|
+
let totalBytes = 0;
|
|
14018
|
+
for (const deviceId of deviceIds) {
|
|
14019
|
+
const rows = await eventStore.countForDevice(deviceId);
|
|
14020
|
+
const bytes = mediaByDevice.get(deviceId)?.bytes ?? 0;
|
|
14021
|
+
if (rows === 0 && bytes === 0) continue;
|
|
14022
|
+
totalRows += rows;
|
|
14023
|
+
totalBytes += bytes;
|
|
14024
|
+
devices.push({
|
|
14025
|
+
deviceId,
|
|
14026
|
+
rows,
|
|
14027
|
+
bytes
|
|
14028
|
+
});
|
|
14029
|
+
}
|
|
14030
|
+
devices.sort((a, b) => b.bytes - a.bytes);
|
|
14031
|
+
return {
|
|
14032
|
+
totalRows,
|
|
14033
|
+
totalBytes,
|
|
14034
|
+
devices
|
|
14035
|
+
};
|
|
14036
|
+
}
|
|
14037
|
+
/**
|
|
14038
|
+
* Cluster-wide prune of events with `timestamp < olderThanMs` across every
|
|
14039
|
+
* camera (via the device-agnostic `evictBefore`), deleting each pruned event's
|
|
14040
|
+
* media in lockstep. Logs one global (`deviceId:null`) events ops-log row.
|
|
14041
|
+
* `bytesReclaimed` is 0 (media deletion does not surface freed bytes).
|
|
14042
|
+
*/
|
|
14043
|
+
async pruneEvents(input) {
|
|
14044
|
+
const eventStore = this.eventStore;
|
|
14045
|
+
if (!eventStore) return {
|
|
14046
|
+
motion: 0,
|
|
14047
|
+
object: 0,
|
|
14048
|
+
audio: 0
|
|
14049
|
+
};
|
|
14050
|
+
const cutoff = input.olderThanMs;
|
|
14051
|
+
const evicted = await eventStore.evictBefore({
|
|
14052
|
+
motionCutoffMs: cutoff,
|
|
14053
|
+
objectCutoffMs: cutoff,
|
|
14054
|
+
audioCutoffMs: cutoff
|
|
14055
|
+
});
|
|
14056
|
+
const ids = [
|
|
14057
|
+
...evicted.motion,
|
|
14058
|
+
...evicted.object,
|
|
14059
|
+
...evicted.audio
|
|
14060
|
+
];
|
|
14061
|
+
if (ids.length > 0 && this.mediaStore) await this.mediaStore.deleteForEvents(ids);
|
|
14062
|
+
const counts = {
|
|
14063
|
+
motion: evicted.motion.length,
|
|
14064
|
+
object: evicted.object.length,
|
|
14065
|
+
audio: evicted.audio.length
|
|
14066
|
+
};
|
|
14067
|
+
const total = counts.motion + counts.object + counts.audio;
|
|
14068
|
+
await this.eventsOpsLog?.append({
|
|
14069
|
+
op: "prune",
|
|
14070
|
+
reason: input.reason ?? "retention",
|
|
14071
|
+
deviceId: null,
|
|
14072
|
+
itemsAffected: total,
|
|
14073
|
+
bytesReclaimed: 0,
|
|
14074
|
+
detail: `pruned events older than ${cutoff}`
|
|
14075
|
+
});
|
|
14076
|
+
if (total > 0) this.ctx.logger.info("analytics events pruned (cluster)", { meta: {
|
|
14077
|
+
cutoffMs: cutoff,
|
|
14078
|
+
...counts
|
|
14079
|
+
} });
|
|
14080
|
+
return counts;
|
|
14081
|
+
}
|
|
14082
|
+
/**
|
|
14083
|
+
* Manually delete EVERY event (motion + object + audio) for one camera and its
|
|
14084
|
+
* event-owned media in lockstep. Logs a manual events ops-log row.
|
|
14085
|
+
*/
|
|
14086
|
+
async deleteDeviceEvents(input) {
|
|
14087
|
+
if (!this.eventStore) return {
|
|
14088
|
+
motion: 0,
|
|
14089
|
+
object: 0,
|
|
14090
|
+
audio: 0
|
|
14091
|
+
};
|
|
14092
|
+
const counts = await this.pruneEventsBefore({
|
|
14093
|
+
deviceId: input.deviceId,
|
|
14094
|
+
cutoffMs: Number.MAX_SAFE_INTEGER
|
|
14095
|
+
});
|
|
14096
|
+
const total = counts.motion + counts.object + counts.audio;
|
|
14097
|
+
await this.eventsOpsLog?.append({
|
|
14098
|
+
op: "manual-delete",
|
|
14099
|
+
reason: "manual",
|
|
14100
|
+
deviceId: input.deviceId,
|
|
14101
|
+
itemsAffected: total,
|
|
14102
|
+
bytesReclaimed: 0,
|
|
14103
|
+
detail: "deleted all events for device"
|
|
14104
|
+
});
|
|
14105
|
+
return counts;
|
|
14106
|
+
}
|
|
14107
|
+
/** The events ops-log rows (newest-first), optionally scoped to one camera. */
|
|
14108
|
+
async listOpsLog(input) {
|
|
14109
|
+
return await this.eventsOpsLog?.list(input) ?? [];
|
|
14110
|
+
}
|
|
14111
|
+
/**
|
|
13443
14112
|
* Track-centric time-based retention (design §5.1). Drains every persisted
|
|
13444
14113
|
* track for the device whose `lastSeen < cutoffMs`, page by page, through the
|
|
13445
14114
|
* widened cascade — enrolled faces/plates + identity media are exempt (design
|
|
@@ -13819,6 +14488,51 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13819
14488
|
}
|
|
13820
14489
|
]
|
|
13821
14490
|
},
|
|
14491
|
+
{
|
|
14492
|
+
id: "package-drop",
|
|
14493
|
+
title: "Package detection",
|
|
14494
|
+
description: "Detect a delivered package (a package-class object left parked inside a package zone) and, symmetrically, its pick-up. Draw the package zone in the zone editor; enable the package class in the object-detection step. Per-device override.",
|
|
14495
|
+
columns: 2,
|
|
14496
|
+
fields: [
|
|
14497
|
+
{
|
|
14498
|
+
type: "boolean",
|
|
14499
|
+
key: "packageDropEnabled",
|
|
14500
|
+
label: "Enable package detection",
|
|
14501
|
+
description: "Turn package delivered / picked-up detection on for this camera (off elsewhere).",
|
|
14502
|
+
default: PACKAGE_DROP_DEFAULTS.packageDropEnabled
|
|
14503
|
+
},
|
|
14504
|
+
{
|
|
14505
|
+
type: "boolean",
|
|
14506
|
+
key: "packageDropPickupEnabled",
|
|
14507
|
+
label: "Emit pick-up",
|
|
14508
|
+
description: "Also emit a package-picked-up event when the delivered package leaves.",
|
|
14509
|
+
default: PACKAGE_DROP_DEFAULTS.packageDropPickupEnabled
|
|
14510
|
+
},
|
|
14511
|
+
{
|
|
14512
|
+
type: "slider",
|
|
14513
|
+
key: "packageDropDwellSec",
|
|
14514
|
+
label: "Minimum dwell",
|
|
14515
|
+
description: "How long a package must stay parked before it counts as a delivery. Higher = fewer false positives from bags briefly set down.",
|
|
14516
|
+
min: 0,
|
|
14517
|
+
max: 300,
|
|
14518
|
+
step: 5,
|
|
14519
|
+
default: PACKAGE_DROP_DEFAULTS.packageDropDwellSec,
|
|
14520
|
+
showValue: true,
|
|
14521
|
+
unit: "s"
|
|
14522
|
+
},
|
|
14523
|
+
{
|
|
14524
|
+
type: "slider",
|
|
14525
|
+
key: "packageDropMinBboxAreaFrac",
|
|
14526
|
+
label: "Minimum package size",
|
|
14527
|
+
description: "Reject tiny far-field blobs: the package box must cover at least this fraction of the frame.",
|
|
14528
|
+
min: 0,
|
|
14529
|
+
max: .1,
|
|
14530
|
+
step: .001,
|
|
14531
|
+
default: PACKAGE_DROP_DEFAULTS.packageDropMinBboxAreaFrac,
|
|
14532
|
+
showValue: true
|
|
14533
|
+
}
|
|
14534
|
+
]
|
|
14535
|
+
},
|
|
13822
14536
|
{
|
|
13823
14537
|
id: "audio-detection",
|
|
13824
14538
|
title: "Audio detection",
|