@camstack/addon-post-analysis 1.2.151 → 1.2.152
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-DpQno5NK.js → dist-Bw6cidiJ.js} +295 -193
- package/dist/{dist-B5K63Zmt.mjs → dist-DSg_jDMt.mjs} +295 -193
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-bK-ELdFD.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DTO1HvN7.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-DxVeMmnD.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-CgUMPlDe.mjs → hostInit-r4RyRXk7.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +686 -474
- package/dist/pipeline-analytics/index.mjs +685 -473
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D8CtX8cU.mjs +0 -26
|
@@ -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-Bw6cidiJ.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");
|
|
@@ -5413,12 +5413,6 @@ function buildAlarmButtons(input) {
|
|
|
5413
5413
|
}];
|
|
5414
5414
|
}
|
|
5415
5415
|
//#endregion
|
|
5416
|
-
//#region src/notification-center/cap-caller.ts
|
|
5417
|
-
function requireCapCaller(method, caller) {
|
|
5418
|
-
if (caller === void 0 || typeof caller.userId !== "string" || caller.userId.length === 0) throw new Error(`notification-rules.${method} declares caller: 'required' and this call carried no caller identity — an addon-to-addon cap call has no authenticated user`);
|
|
5419
|
-
return caller;
|
|
5420
|
-
}
|
|
5421
|
-
//#endregion
|
|
5422
5416
|
//#region src/notification-center/audio-rule-matcher.ts
|
|
5423
5417
|
/**
|
|
5424
5418
|
* AudioWatcher — the Notification Center's matcher for audio rules
|
|
@@ -5661,6 +5655,12 @@ var AudioWatcher = class {
|
|
|
5661
5655
|
}
|
|
5662
5656
|
};
|
|
5663
5657
|
//#endregion
|
|
5658
|
+
//#region src/notification-center/cap-caller.ts
|
|
5659
|
+
function requireCapCaller(method, caller) {
|
|
5660
|
+
if (caller === void 0 || typeof caller.userId !== "string" || caller.userId.length === 0) throw new Error(`notification-rules.${method} declares caller: 'required' and this call carried no caller identity — an addon-to-addon cap call has no authenticated user`);
|
|
5661
|
+
return caller;
|
|
5662
|
+
}
|
|
5663
|
+
//#endregion
|
|
5664
5664
|
//#region src/shared/llm-vision/prompt-hygiene.ts
|
|
5665
5665
|
/**
|
|
5666
5666
|
* What a PIPELINE VALUE is allowed to look like once it is inside a prompt.
|
|
@@ -6295,25 +6295,46 @@ var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
|
6295
6295
|
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
6296
6296
|
var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
|
|
6297
6297
|
/**
|
|
6298
|
-
* Kinds whose action is "go and look at
|
|
6298
|
+
* Kinds whose action is "go and look at this device's stream".
|
|
6299
|
+
*
|
|
6300
|
+
* BOTH DIRECTIONS. A recovery lands on the same page as the failure it undoes,
|
|
6301
|
+
* because "did it really come back, and what did it do while it was gone" is
|
|
6302
|
+
* an action and the page that answers it is the one the down-notification
|
|
6303
|
+
* already opened. On 2026-08-30 the operator's phone showed "Il dispositivo
|
|
6304
|
+
* Videocamera sala feste è online" with `tapAction.value: null` — the same
|
|
6305
|
+
* camera's `device-offline` an hour earlier opened the Stream Broker tab, and
|
|
6306
|
+
* the recovery opened nothing, purely because `device-online` had no entry
|
|
6307
|
+
* here.
|
|
6299
6308
|
*
|
|
6300
|
-
* The legacy `camera
|
|
6301
|
-
* enum: outbox and history rows enqueued before the rename carry
|
|
6309
|
+
* The legacy `camera-*` pair is here for the same reason both are still in the
|
|
6310
|
+
* enum: outbox and history rows enqueued before the rename carry them, and an
|
|
6302
6311
|
* old row is exactly the one whose action has to keep working.
|
|
6312
|
+
*
|
|
6313
|
+
* Kinds NOT here are kinds whose DOWN direction has no action either
|
|
6314
|
+
* (`node-online`/`node-offline`, `device-enabled`/`device-disabled`) — a
|
|
6315
|
+
* recovery does not earn a destination its failure was never given.
|
|
6303
6316
|
*/
|
|
6304
|
-
var
|
|
6317
|
+
var DEVICE_STREAM_KINDS = new Set([
|
|
6305
6318
|
"device-offline",
|
|
6319
|
+
"device-online",
|
|
6306
6320
|
"stream-offline",
|
|
6307
|
-
"
|
|
6321
|
+
"stream-online",
|
|
6322
|
+
"camera-offline",
|
|
6323
|
+
"camera-online"
|
|
6308
6324
|
]);
|
|
6309
6325
|
/**
|
|
6310
6326
|
* Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
|
|
6311
6327
|
*
|
|
6312
|
-
* A stream belongs to a camera and nothing else. The legacy `camera
|
|
6313
|
-
*
|
|
6314
|
-
*
|
|
6328
|
+
* A stream belongs to a camera and nothing else. The legacy `camera-*` pair is
|
|
6329
|
+
* too — that name is precisely the claim it made, and it is why the kind was
|
|
6330
|
+
* split into `device-*` + a `deviceTypes` condition.
|
|
6315
6331
|
*/
|
|
6316
|
-
var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
|
|
6332
|
+
var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
|
|
6333
|
+
"stream-offline",
|
|
6334
|
+
"stream-online",
|
|
6335
|
+
"camera-offline",
|
|
6336
|
+
"camera-online"
|
|
6337
|
+
]);
|
|
6317
6338
|
function withParam(path, key, value) {
|
|
6318
6339
|
if (value === void 0 || value.length === 0) return path;
|
|
6319
6340
|
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
@@ -6328,7 +6349,7 @@ function withParam(path, key, value) {
|
|
|
6328
6349
|
function systemEventTapPath(subject) {
|
|
6329
6350
|
if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
|
|
6330
6351
|
if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
|
|
6331
|
-
if (
|
|
6352
|
+
if (DEVICE_STREAM_KINDS.has(subject.kind)) {
|
|
6332
6353
|
const deviceId = subject.deviceId;
|
|
6333
6354
|
if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
6334
6355
|
const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
|
|
@@ -10730,16 +10751,26 @@ function incomingFromUpdateAvailable(facts, data) {
|
|
|
10730
10751
|
...nodeIds.length > 0 ? { nodeIds } : {}
|
|
10731
10752
|
});
|
|
10732
10753
|
}
|
|
10733
|
-
/**
|
|
10754
|
+
/**
|
|
10755
|
+
* Version-bearing `addon.updated` only — a settings save carries no versions
|
|
10756
|
+
* at all (`BaseAddon.updateGlobalSettings`).
|
|
10757
|
+
*
|
|
10758
|
+
* `toVersion` IS the discriminator, and `fromVersion` is not: the emitter
|
|
10759
|
+
* omits the predecessor when it cannot read what was installed (a first
|
|
10760
|
+
* install, an unreadable tree). Requiring both dropped those updates on the
|
|
10761
|
+
* floor entirely — the operator got no notification for an update that
|
|
10762
|
+
* happened — which is the price that was being paid to keep `'0.0.0'`
|
|
10763
|
+
* out. Report the half that is known; say nothing about the half that is not.
|
|
10764
|
+
*/
|
|
10734
10765
|
function parseAddonUpdateComplete(data) {
|
|
10735
10766
|
const packageName = stringField(data, "packageName");
|
|
10736
10767
|
const fromVersion = stringField(data, "fromVersion");
|
|
10737
10768
|
const toVersion = stringField(data, "toVersion");
|
|
10738
|
-
if (packageName === void 0 ||
|
|
10769
|
+
if (packageName === void 0 || toVersion === void 0) return null;
|
|
10739
10770
|
const nodeId = stringField(data, "agent") ?? stringField(data, "nodeId");
|
|
10740
10771
|
return {
|
|
10741
10772
|
packageName,
|
|
10742
|
-
fromVersion,
|
|
10773
|
+
...fromVersion !== void 0 ? { fromVersion } : {},
|
|
10743
10774
|
toVersion,
|
|
10744
10775
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10745
10776
|
};
|
|
@@ -10749,7 +10780,10 @@ function incomingFromAddonUpdates(facts, updates) {
|
|
|
10749
10780
|
const packageNames = unique(updates.map((update) => update.packageName));
|
|
10750
10781
|
const nodeIds = unique(updates.map((update) => update.nodeId).filter((id) => id !== void 0));
|
|
10751
10782
|
const signature = updates.map((update) => `${update.nodeId ?? ""}:${update.packageName}@${update.toVersion}`).toSorted().join(",");
|
|
10752
|
-
const list = updates.map((update) =>
|
|
10783
|
+
const list = updates.map((update) => {
|
|
10784
|
+
const transition = update.fromVersion !== void 0 ? `${update.fromVersion} → ${update.toVersion}` : `→ ${update.toVersion}`;
|
|
10785
|
+
return update.nodeId !== void 0 ? `${update.packageName} ${transition} (${update.nodeId})` : `${update.packageName} ${transition}`;
|
|
10786
|
+
}).join("\n");
|
|
10753
10787
|
const head = updates[0];
|
|
10754
10788
|
return systemSubject({
|
|
10755
10789
|
facts,
|
|
@@ -23586,17 +23620,17 @@ function planRuleTest(input) {
|
|
|
23586
23620
|
deviceName: `camera ${String(systemScope.deviceId ?? deviceId)}`,
|
|
23587
23621
|
nodeId: systemScope.nodeId ?? "hub",
|
|
23588
23622
|
packageName: systemScope.packageName ?? "@camstack/server",
|
|
23589
|
-
currentVersion: "
|
|
23590
|
-
latestVersion: "
|
|
23623
|
+
currentVersion: "1.0.0",
|
|
23624
|
+
latestVersion: "1.0.1",
|
|
23591
23625
|
profile: "low",
|
|
23592
23626
|
seconds: "30",
|
|
23593
23627
|
devices: "cuda:0",
|
|
23594
23628
|
minutes: "15",
|
|
23595
23629
|
addons: TEST_CRASHED_RUNNER,
|
|
23596
|
-
list: "@camstack/addon-pipeline 1.0.
|
|
23630
|
+
list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
|
|
23597
23631
|
nodes: "hub",
|
|
23598
|
-
fromVersion: "
|
|
23599
|
-
toVersion: "
|
|
23632
|
+
fromVersion: "1.0.0",
|
|
23633
|
+
toVersion: "1.0.1",
|
|
23600
23634
|
title: "Export",
|
|
23601
23635
|
count: "1"
|
|
23602
23636
|
},
|
|
@@ -25746,7 +25780,7 @@ var MediaStore = class {
|
|
|
25746
25780
|
});
|
|
25747
25781
|
}
|
|
25748
25782
|
/** Persist a media blob and register its metadata row. Returns the
|
|
25749
|
-
* collection key (
|
|
25783
|
+
* collection key (MediaFileRef.key) that callers store as reference. */
|
|
25750
25784
|
async put(params) {
|
|
25751
25785
|
if (this.writeGate) return this.writeGate.run(() => this.putUnchecked(params));
|
|
25752
25786
|
return this.putUnchecked(params);
|
|
@@ -26068,8 +26102,20 @@ var MediaStore = class {
|
|
|
26068
26102
|
* its device at write time, so scoping the query by it makes a media read
|
|
26069
26103
|
* for a device the caller cannot name return nothing — the DB itself
|
|
26070
26104
|
* enforces the boundary, no post-hoc ownership check to forget.
|
|
26105
|
+
*
|
|
26106
|
+
* `kinds` is the WORK filter, and it belongs here rather than on the answer.
|
|
26107
|
+
* Every row this returns has had its blob read off disk and base64-encoded,
|
|
26108
|
+
* so a caller that filters afterwards has already paid for every kind it is
|
|
26109
|
+
* about to discard: measured on the live hub, one track's six kinds are
|
|
26110
|
+
* 3 597 219 bytes and `thumbnailSmall` is 31 155 of them, and asking for the
|
|
26111
|
+
* small one alone cost exactly as much as asking for all six. `kind` is a
|
|
26112
|
+
* real column (see {@link MEDIA_COLUMNS}), so the narrowing happens in the
|
|
26113
|
+
* query and the blobs of the other kinds are never opened.
|
|
26114
|
+
*
|
|
26115
|
+
* Absent (or empty) means every kind — the back-compat default, and the
|
|
26116
|
+
* honest answer for a caller that has not decided what it needs.
|
|
26071
26117
|
*/
|
|
26072
|
-
async listByOwner(ownerKind, ownerId, deviceId) {
|
|
26118
|
+
async listByOwner(ownerKind, ownerId, deviceId, kinds) {
|
|
26073
26119
|
const rows = await this.store.query.query({
|
|
26074
26120
|
collection: MEDIA_COLLECTION,
|
|
26075
26121
|
filter: {
|
|
@@ -26081,6 +26127,7 @@ var MediaStore = class {
|
|
|
26081
26127
|
ownerId,
|
|
26082
26128
|
deviceId
|
|
26083
26129
|
},
|
|
26130
|
+
...kinds !== void 0 && kinds.length > 0 ? { whereIn: { kind: [...kinds] } } : {},
|
|
26084
26131
|
orderBy: {
|
|
26085
26132
|
field: "timestamp",
|
|
26086
26133
|
direction: "asc"
|
|
@@ -27997,6 +28044,63 @@ var embeddingActions = require_dist.defineCustomActions({
|
|
|
27997
28044
|
"embedding.reembedFacesStatus": require_dist.customAction(NoInputSchema$1, ReembedFacesStatusSchema)
|
|
27998
28045
|
});
|
|
27999
28046
|
//#endregion
|
|
28047
|
+
//#region src/pipeline-analytics/media-file-url.ts
|
|
28048
|
+
/**
|
|
28049
|
+
* The plane path for one stored media key.
|
|
28050
|
+
*
|
|
28051
|
+
* `prefix` is `/addon/<addonId>/event-media` — a pure function of the addon
|
|
28052
|
+
* id, so it exists whether or not `dataPlane.serve` succeeded. A row that
|
|
28053
|
+
* named no address at all would be the silent drop this repo keeps paying for:
|
|
28054
|
+
* a client cannot tell "no bytes" from "no plane", and renders neither.
|
|
28055
|
+
*/
|
|
28056
|
+
function mediaFileUrl(prefix, key) {
|
|
28057
|
+
return `${prefix}/${encodeURIComponent(key)}`;
|
|
28058
|
+
}
|
|
28059
|
+
/**
|
|
28060
|
+
* A stored row as the wire sees it: the ref, the address, and — transitionally
|
|
28061
|
+
* — the bytes.
|
|
28062
|
+
*
|
|
28063
|
+
* `base64` is copied through ONLY because the deployed viewer's track-detail
|
|
28064
|
+
* hero still requires the field to parse a row at all. See the `MediaFile`
|
|
28065
|
+
* docblock in `pipeline-analytics.cap.ts`; deleting the field is a one-line
|
|
28066
|
+
* change here and one in the schema.
|
|
28067
|
+
*
|
|
28068
|
+
* `provenance` is deliberately NOT spread: it is store-internal, and the
|
|
28069
|
+
* previous `{ ...row, kind }` shape published it by accident.
|
|
28070
|
+
*/
|
|
28071
|
+
function toWireMediaFile(row, prefix) {
|
|
28072
|
+
return {
|
|
28073
|
+
key: row.key,
|
|
28074
|
+
kind: row.kind,
|
|
28075
|
+
sizeBytes: row.sizeBytes,
|
|
28076
|
+
timestamp: row.timestamp,
|
|
28077
|
+
url: mediaFileUrl(prefix, row.key),
|
|
28078
|
+
base64: row.base64
|
|
28079
|
+
};
|
|
28080
|
+
}
|
|
28081
|
+
/** The same row without its bytes — the manifest shape. Carries the SAME
|
|
28082
|
+
* `url` as the byte read, so a client never builds the plane path itself. */
|
|
28083
|
+
function toWireMediaFileInfo(row, prefix) {
|
|
28084
|
+
return {
|
|
28085
|
+
key: row.key,
|
|
28086
|
+
kind: row.kind,
|
|
28087
|
+
sizeBytes: row.sizeBytes,
|
|
28088
|
+
timestamp: row.timestamp,
|
|
28089
|
+
url: mediaFileUrl(prefix, row.key)
|
|
28090
|
+
};
|
|
28091
|
+
}
|
|
28092
|
+
//#endregion
|
|
28093
|
+
//#region src/pipeline-analytics/gallery-media-row.ts
|
|
28094
|
+
function toGalleryMediaRow(row, prefix) {
|
|
28095
|
+
return {
|
|
28096
|
+
key: row.key,
|
|
28097
|
+
kind: row.kind,
|
|
28098
|
+
url: mediaFileUrl(prefix, row.key),
|
|
28099
|
+
sizeBytes: row.sizeBytes,
|
|
28100
|
+
timestamp: row.timestamp
|
|
28101
|
+
};
|
|
28102
|
+
}
|
|
28103
|
+
//#endregion
|
|
28000
28104
|
//#region src/pipeline-analytics/include-crops-default.ts
|
|
28001
28105
|
function resolveIncludeCrops(input) {
|
|
28002
28106
|
return input?.includeCrops === true;
|
|
@@ -29656,6 +29760,7 @@ var FaceGalleryProvider = class {
|
|
|
29656
29760
|
logger;
|
|
29657
29761
|
refreshGallery;
|
|
29658
29762
|
eventMediaBaseUrl;
|
|
29763
|
+
eventMediaPathPrefix;
|
|
29659
29764
|
emitFaceGalleryChanged;
|
|
29660
29765
|
constructor(deps) {
|
|
29661
29766
|
this.identityStore = deps.identityStore;
|
|
@@ -29666,6 +29771,7 @@ var FaceGalleryProvider = class {
|
|
|
29666
29771
|
this.logger = deps.logger;
|
|
29667
29772
|
this.refreshGallery = deps.refreshGallery;
|
|
29668
29773
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
29774
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
29669
29775
|
this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
|
|
29670
29776
|
}
|
|
29671
29777
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
@@ -29853,13 +29959,7 @@ var FaceGalleryProvider = class {
|
|
|
29853
29959
|
* the enrolled identity copies — those live under identity owner).
|
|
29854
29960
|
*/
|
|
29855
29961
|
async getFaceMedia(input) {
|
|
29856
|
-
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => (
|
|
29857
|
-
key: m.key,
|
|
29858
|
-
kind: m.kind,
|
|
29859
|
-
base64: m.base64,
|
|
29860
|
-
sizeBytes: m.sizeBytes,
|
|
29861
|
-
timestamp: m.timestamp
|
|
29862
|
-
}));
|
|
29962
|
+
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
29863
29963
|
}
|
|
29864
29964
|
/**
|
|
29865
29965
|
* Assign a buffered face to an identity:
|
|
@@ -32572,50 +32672,136 @@ var FrameProcessor = class {
|
|
|
32572
32672
|
};
|
|
32573
32673
|
}
|
|
32574
32674
|
};
|
|
32575
|
-
|
|
32576
|
-
|
|
32675
|
+
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
32676
|
+
* evicts the oldest record. */
|
|
32677
|
+
var PendingPreviewUpgrades = class {
|
|
32678
|
+
entries = /* @__PURE__ */ new Map();
|
|
32679
|
+
ttlMs;
|
|
32680
|
+
maxEntries;
|
|
32681
|
+
now;
|
|
32682
|
+
constructor(options = {}) {
|
|
32683
|
+
this.ttlMs = options.ttlMs ?? 9e5;
|
|
32684
|
+
this.maxEntries = Math.max(1, options.maxEntries ?? 512);
|
|
32685
|
+
this.now = options.now ?? (() => Date.now());
|
|
32686
|
+
}
|
|
32687
|
+
/** Record (or replace) a closed track's pending upgrade. Prunes expired
|
|
32688
|
+
* entries first, then evicts the oldest while over the cap. */
|
|
32689
|
+
record(entry) {
|
|
32690
|
+
this.prune();
|
|
32691
|
+
this.entries.delete(entry.trackId);
|
|
32692
|
+
this.entries.set(entry.trackId, entry);
|
|
32693
|
+
while (this.entries.size > this.maxEntries) {
|
|
32694
|
+
const oldest = this.entries.keys().next();
|
|
32695
|
+
if (oldest.done === true) break;
|
|
32696
|
+
this.entries.delete(oldest.value);
|
|
32697
|
+
}
|
|
32698
|
+
}
|
|
32699
|
+
/** Claim a track's pending upgrade — removes it, so a second landing (or a
|
|
32700
|
+
* concurrent racer) finds nothing and the tile is rewritten at most once. */
|
|
32701
|
+
take(trackId) {
|
|
32702
|
+
this.prune();
|
|
32703
|
+
const entry = this.entries.get(trackId);
|
|
32704
|
+
if (entry === void 0) return void 0;
|
|
32705
|
+
this.entries.delete(trackId);
|
|
32706
|
+
return entry;
|
|
32707
|
+
}
|
|
32708
|
+
get size() {
|
|
32709
|
+
return this.entries.size;
|
|
32710
|
+
}
|
|
32711
|
+
prune() {
|
|
32712
|
+
const cutoff = this.now() - this.ttlMs;
|
|
32713
|
+
for (const [trackId, entry] of this.entries) if (entry.recordedAtMs < cutoff) this.entries.delete(trackId);
|
|
32714
|
+
}
|
|
32715
|
+
};
|
|
32577
32716
|
/**
|
|
32578
|
-
*
|
|
32579
|
-
*
|
|
32580
|
-
*
|
|
32581
|
-
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
32582
|
-
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
32717
|
+
* Redeem a closed track's pending preview off a keyFrame that landed late.
|
|
32718
|
+
* Every non-`upgraded` outcome that had a pending entry logs itself — a
|
|
32719
|
+
* branch that leaves the stand-in in place must say so.
|
|
32583
32720
|
*/
|
|
32584
|
-
function
|
|
32585
|
-
const
|
|
32586
|
-
|
|
32587
|
-
if (
|
|
32588
|
-
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
|
|
32592
|
-
|
|
32593
|
-
frame
|
|
32721
|
+
async function runPreviewUpgrade(deps, input) {
|
|
32722
|
+
const { deviceId, trackId } = input;
|
|
32723
|
+
const pending = deps.takePending(trackId);
|
|
32724
|
+
if (pending === void 0) return "no-pending";
|
|
32725
|
+
const held = await deps.heldThumbnailProvenance(trackId);
|
|
32726
|
+
if (held !== null && mediaProvenanceRank(held) >= mediaProvenanceRank("native")) {
|
|
32727
|
+
deps.logger.info("late keyFrame: preview already native — no upgrade", {
|
|
32728
|
+
tags: { deviceId },
|
|
32729
|
+
meta: { trackId }
|
|
32594
32730
|
});
|
|
32595
|
-
|
|
32596
|
-
|
|
32597
|
-
|
|
32598
|
-
|
|
32599
|
-
|
|
32600
|
-
|
|
32601
|
-
|
|
32602
|
-
|
|
32603
|
-
framesMatched: matched
|
|
32604
|
-
});
|
|
32605
|
-
else {
|
|
32606
|
-
existing.className = td.className;
|
|
32607
|
-
existing.lastSeenMs = timestamp;
|
|
32608
|
-
existing.framesMatched += matched;
|
|
32731
|
+
return "held-native";
|
|
32732
|
+
}
|
|
32733
|
+
if (pending.bestSeen === void 0) {
|
|
32734
|
+
deps.logger.info("late keyFrame landed but no subject bbox was ever seen — tile kept as-is", {
|
|
32735
|
+
tags: { deviceId },
|
|
32736
|
+
meta: {
|
|
32737
|
+
trackId,
|
|
32738
|
+
heldProvenance: held
|
|
32609
32739
|
}
|
|
32610
|
-
}
|
|
32740
|
+
});
|
|
32741
|
+
return "no-bbox";
|
|
32611
32742
|
}
|
|
32612
|
-
|
|
32613
|
-
|
|
32614
|
-
|
|
32615
|
-
|
|
32616
|
-
|
|
32617
|
-
|
|
32618
|
-
|
|
32743
|
+
if (pending.bestSeen.timestamp === void 0) {
|
|
32744
|
+
deps.logger.info("late keyFrame: pending entry predates the motion guard — tile kept as-is", {
|
|
32745
|
+
tags: { deviceId },
|
|
32746
|
+
meta: {
|
|
32747
|
+
trackId,
|
|
32748
|
+
keyFrameKey: input.keyFrameKey,
|
|
32749
|
+
heldProvenance: held
|
|
32750
|
+
}
|
|
32751
|
+
});
|
|
32752
|
+
return "no-bbox";
|
|
32753
|
+
}
|
|
32754
|
+
const derived = await deps.derive({
|
|
32755
|
+
deviceId,
|
|
32756
|
+
mediaKey: input.keyFrameKey,
|
|
32757
|
+
bbox: {
|
|
32758
|
+
x: pending.bestSeen.x,
|
|
32759
|
+
y: pending.bestSeen.y,
|
|
32760
|
+
w: pending.bestSeen.w,
|
|
32761
|
+
h: pending.bestSeen.h
|
|
32762
|
+
},
|
|
32763
|
+
frameWidth: pending.bestSeen.frameWidth,
|
|
32764
|
+
frameHeight: pending.bestSeen.frameHeight,
|
|
32765
|
+
timestamp: pending.bestSeen.timestamp
|
|
32766
|
+
});
|
|
32767
|
+
if (derived === null) {
|
|
32768
|
+
deps.logger.info("late keyFrame preview upgrade failed (derive miss) — tile kept as-is", {
|
|
32769
|
+
tags: { deviceId },
|
|
32770
|
+
meta: {
|
|
32771
|
+
trackId,
|
|
32772
|
+
keyFrameKey: input.keyFrameKey,
|
|
32773
|
+
heldProvenance: held
|
|
32774
|
+
}
|
|
32775
|
+
});
|
|
32776
|
+
return "derive-miss";
|
|
32777
|
+
}
|
|
32778
|
+
await deps.put({
|
|
32779
|
+
deviceId,
|
|
32780
|
+
trackId,
|
|
32781
|
+
kind: "thumbnail",
|
|
32782
|
+
timestamp: input.timestamp,
|
|
32783
|
+
data: derived.thumbnail,
|
|
32784
|
+
provenance: "native"
|
|
32785
|
+
});
|
|
32786
|
+
await deps.put({
|
|
32787
|
+
deviceId,
|
|
32788
|
+
trackId,
|
|
32789
|
+
kind: "thumbnailSmall",
|
|
32790
|
+
timestamp: input.timestamp,
|
|
32791
|
+
data: derived.thumbnailSmall,
|
|
32792
|
+
provenance: "native"
|
|
32793
|
+
});
|
|
32794
|
+
await deps.clearPreviewMiss(deviceId, trackId);
|
|
32795
|
+
deps.logger.info("best-shot upgraded from late keyFrame (stand-in replaced)", {
|
|
32796
|
+
tags: { deviceId },
|
|
32797
|
+
meta: {
|
|
32798
|
+
trackId,
|
|
32799
|
+
keyFrameKey: input.keyFrameKey,
|
|
32800
|
+
replaced: held ?? "none",
|
|
32801
|
+
skewMs: derived.skewMs
|
|
32802
|
+
}
|
|
32803
|
+
});
|
|
32804
|
+
return "upgraded";
|
|
32619
32805
|
}
|
|
32620
32806
|
function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, score) {
|
|
32621
32807
|
if (frameWidth <= 0 || frameHeight <= 0) return true;
|
|
@@ -32775,137 +32961,6 @@ function promoteSemanticBestFrame(deps, input) {
|
|
|
32775
32961
|
captured: true
|
|
32776
32962
|
};
|
|
32777
32963
|
}
|
|
32778
|
-
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
32779
|
-
* evicts the oldest record. */
|
|
32780
|
-
var PendingPreviewUpgrades = class {
|
|
32781
|
-
entries = /* @__PURE__ */ new Map();
|
|
32782
|
-
ttlMs;
|
|
32783
|
-
maxEntries;
|
|
32784
|
-
now;
|
|
32785
|
-
constructor(options = {}) {
|
|
32786
|
-
this.ttlMs = options.ttlMs ?? 9e5;
|
|
32787
|
-
this.maxEntries = Math.max(1, options.maxEntries ?? 512);
|
|
32788
|
-
this.now = options.now ?? (() => Date.now());
|
|
32789
|
-
}
|
|
32790
|
-
/** Record (or replace) a closed track's pending upgrade. Prunes expired
|
|
32791
|
-
* entries first, then evicts the oldest while over the cap. */
|
|
32792
|
-
record(entry) {
|
|
32793
|
-
this.prune();
|
|
32794
|
-
this.entries.delete(entry.trackId);
|
|
32795
|
-
this.entries.set(entry.trackId, entry);
|
|
32796
|
-
while (this.entries.size > this.maxEntries) {
|
|
32797
|
-
const oldest = this.entries.keys().next();
|
|
32798
|
-
if (oldest.done === true) break;
|
|
32799
|
-
this.entries.delete(oldest.value);
|
|
32800
|
-
}
|
|
32801
|
-
}
|
|
32802
|
-
/** Claim a track's pending upgrade — removes it, so a second landing (or a
|
|
32803
|
-
* concurrent racer) finds nothing and the tile is rewritten at most once. */
|
|
32804
|
-
take(trackId) {
|
|
32805
|
-
this.prune();
|
|
32806
|
-
const entry = this.entries.get(trackId);
|
|
32807
|
-
if (entry === void 0) return void 0;
|
|
32808
|
-
this.entries.delete(trackId);
|
|
32809
|
-
return entry;
|
|
32810
|
-
}
|
|
32811
|
-
get size() {
|
|
32812
|
-
return this.entries.size;
|
|
32813
|
-
}
|
|
32814
|
-
prune() {
|
|
32815
|
-
const cutoff = this.now() - this.ttlMs;
|
|
32816
|
-
for (const [trackId, entry] of this.entries) if (entry.recordedAtMs < cutoff) this.entries.delete(trackId);
|
|
32817
|
-
}
|
|
32818
|
-
};
|
|
32819
|
-
/**
|
|
32820
|
-
* Redeem a closed track's pending preview off a keyFrame that landed late.
|
|
32821
|
-
* Every non-`upgraded` outcome that had a pending entry logs itself — a
|
|
32822
|
-
* branch that leaves the stand-in in place must say so.
|
|
32823
|
-
*/
|
|
32824
|
-
async function runPreviewUpgrade(deps, input) {
|
|
32825
|
-
const { deviceId, trackId } = input;
|
|
32826
|
-
const pending = deps.takePending(trackId);
|
|
32827
|
-
if (pending === void 0) return "no-pending";
|
|
32828
|
-
const held = await deps.heldThumbnailProvenance(trackId);
|
|
32829
|
-
if (held !== null && mediaProvenanceRank(held) >= mediaProvenanceRank("native")) {
|
|
32830
|
-
deps.logger.info("late keyFrame: preview already native — no upgrade", {
|
|
32831
|
-
tags: { deviceId },
|
|
32832
|
-
meta: { trackId }
|
|
32833
|
-
});
|
|
32834
|
-
return "held-native";
|
|
32835
|
-
}
|
|
32836
|
-
if (pending.bestSeen === void 0) {
|
|
32837
|
-
deps.logger.info("late keyFrame landed but no subject bbox was ever seen — tile kept as-is", {
|
|
32838
|
-
tags: { deviceId },
|
|
32839
|
-
meta: {
|
|
32840
|
-
trackId,
|
|
32841
|
-
heldProvenance: held
|
|
32842
|
-
}
|
|
32843
|
-
});
|
|
32844
|
-
return "no-bbox";
|
|
32845
|
-
}
|
|
32846
|
-
if (pending.bestSeen.timestamp === void 0) {
|
|
32847
|
-
deps.logger.info("late keyFrame: pending entry predates the motion guard — tile kept as-is", {
|
|
32848
|
-
tags: { deviceId },
|
|
32849
|
-
meta: {
|
|
32850
|
-
trackId,
|
|
32851
|
-
keyFrameKey: input.keyFrameKey,
|
|
32852
|
-
heldProvenance: held
|
|
32853
|
-
}
|
|
32854
|
-
});
|
|
32855
|
-
return "no-bbox";
|
|
32856
|
-
}
|
|
32857
|
-
const derived = await deps.derive({
|
|
32858
|
-
deviceId,
|
|
32859
|
-
mediaKey: input.keyFrameKey,
|
|
32860
|
-
bbox: {
|
|
32861
|
-
x: pending.bestSeen.x,
|
|
32862
|
-
y: pending.bestSeen.y,
|
|
32863
|
-
w: pending.bestSeen.w,
|
|
32864
|
-
h: pending.bestSeen.h
|
|
32865
|
-
},
|
|
32866
|
-
frameWidth: pending.bestSeen.frameWidth,
|
|
32867
|
-
frameHeight: pending.bestSeen.frameHeight,
|
|
32868
|
-
timestamp: pending.bestSeen.timestamp
|
|
32869
|
-
});
|
|
32870
|
-
if (derived === null) {
|
|
32871
|
-
deps.logger.info("late keyFrame preview upgrade failed (derive miss) — tile kept as-is", {
|
|
32872
|
-
tags: { deviceId },
|
|
32873
|
-
meta: {
|
|
32874
|
-
trackId,
|
|
32875
|
-
keyFrameKey: input.keyFrameKey,
|
|
32876
|
-
heldProvenance: held
|
|
32877
|
-
}
|
|
32878
|
-
});
|
|
32879
|
-
return "derive-miss";
|
|
32880
|
-
}
|
|
32881
|
-
await deps.put({
|
|
32882
|
-
deviceId,
|
|
32883
|
-
trackId,
|
|
32884
|
-
kind: "thumbnail",
|
|
32885
|
-
timestamp: input.timestamp,
|
|
32886
|
-
data: derived.thumbnail,
|
|
32887
|
-
provenance: "native"
|
|
32888
|
-
});
|
|
32889
|
-
await deps.put({
|
|
32890
|
-
deviceId,
|
|
32891
|
-
trackId,
|
|
32892
|
-
kind: "thumbnailSmall",
|
|
32893
|
-
timestamp: input.timestamp,
|
|
32894
|
-
data: derived.thumbnailSmall,
|
|
32895
|
-
provenance: "native"
|
|
32896
|
-
});
|
|
32897
|
-
await deps.clearPreviewMiss(deviceId, trackId);
|
|
32898
|
-
deps.logger.info("best-shot upgraded from late keyFrame (stand-in replaced)", {
|
|
32899
|
-
tags: { deviceId },
|
|
32900
|
-
meta: {
|
|
32901
|
-
trackId,
|
|
32902
|
-
keyFrameKey: input.keyFrameKey,
|
|
32903
|
-
replaced: held ?? "none",
|
|
32904
|
-
skewMs: derived.skewMs
|
|
32905
|
-
}
|
|
32906
|
-
});
|
|
32907
|
-
return "upgraded";
|
|
32908
|
-
}
|
|
32909
32964
|
//#endregion
|
|
32910
32965
|
//#region src/pipeline-analytics/store/zone-geometry.ts
|
|
32911
32966
|
/**
|
|
@@ -34644,6 +34699,7 @@ var PlateGalleryProvider = class {
|
|
|
34644
34699
|
refreshGallery;
|
|
34645
34700
|
emitPlateGalleryChanged;
|
|
34646
34701
|
eventMediaBaseUrl;
|
|
34702
|
+
eventMediaPathPrefix;
|
|
34647
34703
|
constructor(deps) {
|
|
34648
34704
|
this.plateStore = deps.plateStore;
|
|
34649
34705
|
this.vehicleStore = deps.vehicleStore;
|
|
@@ -34654,6 +34710,7 @@ var PlateGalleryProvider = class {
|
|
|
34654
34710
|
this.refreshGallery = deps.refreshGallery;
|
|
34655
34711
|
this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
|
|
34656
34712
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
34713
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
34657
34714
|
}
|
|
34658
34715
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
34659
34716
|
safeEmitPlateGalleryChanged(payload) {
|
|
@@ -34724,13 +34781,7 @@ var PlateGalleryProvider = class {
|
|
|
34724
34781
|
return this.toPlateInfo(plate, nameMap, { includeCrops: false });
|
|
34725
34782
|
}
|
|
34726
34783
|
async getPlateMedia(input) {
|
|
34727
|
-
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => (
|
|
34728
|
-
key: m.key,
|
|
34729
|
-
kind: m.kind,
|
|
34730
|
-
base64: m.base64,
|
|
34731
|
-
sizeBytes: m.sizeBytes,
|
|
34732
|
-
timestamp: m.timestamp
|
|
34733
|
-
}));
|
|
34784
|
+
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
34734
34785
|
}
|
|
34735
34786
|
async searchPlates(input) {
|
|
34736
34787
|
const maxDistance = input.maxDistance ?? 1;
|
|
@@ -34973,6 +35024,51 @@ var PlateGalleryProvider = class {
|
|
|
34973
35024
|
}
|
|
34974
35025
|
};
|
|
34975
35026
|
//#endregion
|
|
35027
|
+
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
35028
|
+
/**
|
|
35029
|
+
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
35030
|
+
* return the tracks it produced.
|
|
35031
|
+
*
|
|
35032
|
+
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
35033
|
+
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
35034
|
+
*/
|
|
35035
|
+
function runFrameProcessorPass(input) {
|
|
35036
|
+
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
35037
|
+
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
35038
|
+
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
35039
|
+
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
35040
|
+
const accum = /* @__PURE__ */ new Map();
|
|
35041
|
+
for (const { timestamp, frame } of input.frames) {
|
|
35042
|
+
const result = processor.process({
|
|
35043
|
+
timestamp,
|
|
35044
|
+
frame
|
|
35045
|
+
});
|
|
35046
|
+
for (const td of result.tracked) {
|
|
35047
|
+
const existing = accum.get(td.trackId);
|
|
35048
|
+
const matched = td.matchedThisFrame ? 1 : 0;
|
|
35049
|
+
if (existing === void 0) accum.set(td.trackId, {
|
|
35050
|
+
className: td.className,
|
|
35051
|
+
firstSeenMs: timestamp,
|
|
35052
|
+
lastSeenMs: timestamp,
|
|
35053
|
+
bbox: { ...td.bbox },
|
|
35054
|
+
framesMatched: matched
|
|
35055
|
+
});
|
|
35056
|
+
else {
|
|
35057
|
+
existing.className = td.className;
|
|
35058
|
+
existing.lastSeenMs = timestamp;
|
|
35059
|
+
existing.framesMatched += matched;
|
|
35060
|
+
}
|
|
35061
|
+
}
|
|
35062
|
+
}
|
|
35063
|
+
return { tracks: [...accum.values()].map((t) => ({
|
|
35064
|
+
className: t.className,
|
|
35065
|
+
firstSeenMs: t.firstSeenMs,
|
|
35066
|
+
lastSeenMs: t.lastSeenMs,
|
|
35067
|
+
bbox: t.bbox,
|
|
35068
|
+
framesMatched: t.framesMatched
|
|
35069
|
+
})) };
|
|
35070
|
+
}
|
|
35071
|
+
//#endregion
|
|
34976
35072
|
//#region src/pipeline-analytics/retention/disk-reconcile.ts
|
|
34977
35073
|
/**
|
|
34978
35074
|
* Disk-wins reconcile for pipeline analytics.
|
|
@@ -35037,93 +35133,6 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
35037
35133
|
};
|
|
35038
35134
|
}
|
|
35039
35135
|
//#endregion
|
|
35040
|
-
//#region src/pipeline-analytics/retention/media-reclaim-actions.ts
|
|
35041
|
-
/**
|
|
35042
|
-
* The operator button for the debug-media reclaim.
|
|
35043
|
-
*
|
|
35044
|
-
* The mechanism ships armed with nothing: **arming it is the operator's
|
|
35045
|
-
* call**. `mode` omitted means `report` — it counts, logs one line per camera
|
|
35046
|
-
* and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
|
|
35047
|
-
* that has to be typed.
|
|
35048
|
-
*
|
|
35049
|
-
* Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
|
|
35050
|
-
* earned it: dry-run default, explicit pacing, explicit blast radius, and a
|
|
35051
|
-
* report you can read instead of a log line you have to find.
|
|
35052
|
-
*/
|
|
35053
|
-
/**
|
|
35054
|
-
* `report` counts; `reclaim` deletes.
|
|
35055
|
-
*
|
|
35056
|
-
* `.default('report')` is load-bearing: the settings form sends `{}` for a
|
|
35057
|
-
* button with no fields, and the safe reading of an unstated intent is the one
|
|
35058
|
-
* that destroys nothing.
|
|
35059
|
-
*/
|
|
35060
|
-
var MediaReclaimActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
35061
|
-
var MediaReclaimScopeSchema = require_dist._enum(["motion-still", "track-filmstrip"]);
|
|
35062
|
-
var MediaReclaimActionInputSchema = require_dist.object({
|
|
35063
|
-
mode: MediaReclaimActionModeSchema,
|
|
35064
|
-
/** Which populations to walk. Omitted = both. */
|
|
35065
|
-
scopes: require_dist.array(MediaReclaimScopeSchema).min(1).optional(),
|
|
35066
|
-
/** Restrict to these cameras. Omitted = every camera in retention scope. */
|
|
35067
|
-
deviceIds: require_dist.array(require_dist.number().int()).min(1).optional(),
|
|
35068
|
-
/** Start every device from the top instead of resuming its cursor. */
|
|
35069
|
-
restart: require_dist.boolean().optional(),
|
|
35070
|
-
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
35071
|
-
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
35072
|
-
/** Rows EXAMINED per camera in this pass — the pacing bound. */
|
|
35073
|
-
maxRowsPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
35074
|
-
/** Rows DELETED per camera in this pass — the blast-radius bound. */
|
|
35075
|
-
maxReclaimPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
35076
|
-
/** Bytes released across the whole run — the disk-contention bound. */
|
|
35077
|
-
maxBytesPerRun: require_dist.number().int().min(1).optional(),
|
|
35078
|
-
/** Wall-clock ceiling for the run. */
|
|
35079
|
-
budgetMinutes: require_dist.number().int().min(1).max(720).optional(),
|
|
35080
|
-
/**
|
|
35081
|
-
* Bytes per second the pass may release. The recordings volume sustains
|
|
35082
|
-
* ~4 MB/s and the archive needs it more than this does; the default is half
|
|
35083
|
-
* of that.
|
|
35084
|
-
*/
|
|
35085
|
-
throttleBytesPerSec: require_dist.number().int().min(64 * 1024).optional(),
|
|
35086
|
-
/**
|
|
35087
|
-
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
35088
|
-
* exclusively save rows, never select them.
|
|
35089
|
-
*/
|
|
35090
|
-
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
35091
|
-
});
|
|
35092
|
-
var MediaReclaimDeviceReportSchema = require_dist.object({
|
|
35093
|
-
deviceId: require_dist.number(),
|
|
35094
|
-
scope: MediaReclaimScopeSchema,
|
|
35095
|
-
examined: require_dist.number(),
|
|
35096
|
-
eligible: require_dist.number(),
|
|
35097
|
-
reclaimed: require_dist.number(),
|
|
35098
|
-
bytesReclaimed: require_dist.number(),
|
|
35099
|
-
/** Rows kept because the owning track is pinned (`staging`/`favourited`). */
|
|
35100
|
-
held: require_dist.number(),
|
|
35101
|
-
/** Rows kept because the owner could not be shown to be a motion event. */
|
|
35102
|
-
unresolved: require_dist.number(),
|
|
35103
|
-
withinGrace: require_dist.number(),
|
|
35104
|
-
/** Deletes that REFUSED — the blob survived or the volume could not answer.
|
|
35105
|
-
* The row was kept and the cursor did not pass it. */
|
|
35106
|
-
refused: require_dist.number(),
|
|
35107
|
-
stoppedBy: require_dist.string(),
|
|
35108
|
-
/** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
|
|
35109
|
-
nextCursor: require_dist.number()
|
|
35110
|
-
});
|
|
35111
|
-
var MediaReclaimActionResultSchema = require_dist.object({
|
|
35112
|
-
/** `false` when a pass was already in flight — nothing was done. */
|
|
35113
|
-
ran: require_dist.boolean(),
|
|
35114
|
-
mode: require_dist._enum(["report", "reclaim"]),
|
|
35115
|
-
devices: require_dist.array(MediaReclaimDeviceReportSchema),
|
|
35116
|
-
totalExamined: require_dist.number(),
|
|
35117
|
-
totalEligible: require_dist.number(),
|
|
35118
|
-
totalReclaimed: require_dist.number(),
|
|
35119
|
-
totalBytesReclaimed: require_dist.number(),
|
|
35120
|
-
totalRefused: require_dist.number(),
|
|
35121
|
-
durationMs: require_dist.number(),
|
|
35122
|
-
/** Every device+scope reported `exhausted` — there is nothing left to do. */
|
|
35123
|
-
complete: require_dist.boolean()
|
|
35124
|
-
});
|
|
35125
|
-
var mediaReclaimActions = require_dist.defineCustomActions({ "retention.mediaReclaim": require_dist.customAction(MediaReclaimActionInputSchema, MediaReclaimActionResultSchema, { auth: "admin" }) });
|
|
35126
|
-
//#endregion
|
|
35127
35136
|
//#region src/pipeline-analytics/retention/media-reclaim.ts
|
|
35128
35137
|
var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
|
|
35129
35138
|
var DEFAULT_MEDIA_RECLAIM_LIMITS = {
|
|
@@ -35335,6 +35344,93 @@ async function runMediaReclaim(input) {
|
|
|
35335
35344
|
};
|
|
35336
35345
|
}
|
|
35337
35346
|
//#endregion
|
|
35347
|
+
//#region src/pipeline-analytics/retention/media-reclaim-actions.ts
|
|
35348
|
+
/**
|
|
35349
|
+
* The operator button for the debug-media reclaim.
|
|
35350
|
+
*
|
|
35351
|
+
* The mechanism ships armed with nothing: **arming it is the operator's
|
|
35352
|
+
* call**. `mode` omitted means `report` — it counts, logs one line per camera
|
|
35353
|
+
* and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
|
|
35354
|
+
* that has to be typed.
|
|
35355
|
+
*
|
|
35356
|
+
* Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
|
|
35357
|
+
* earned it: dry-run default, explicit pacing, explicit blast radius, and a
|
|
35358
|
+
* report you can read instead of a log line you have to find.
|
|
35359
|
+
*/
|
|
35360
|
+
/**
|
|
35361
|
+
* `report` counts; `reclaim` deletes.
|
|
35362
|
+
*
|
|
35363
|
+
* `.default('report')` is load-bearing: the settings form sends `{}` for a
|
|
35364
|
+
* button with no fields, and the safe reading of an unstated intent is the one
|
|
35365
|
+
* that destroys nothing.
|
|
35366
|
+
*/
|
|
35367
|
+
var MediaReclaimActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
35368
|
+
var MediaReclaimScopeSchema = require_dist._enum(["motion-still", "track-filmstrip"]);
|
|
35369
|
+
var MediaReclaimActionInputSchema = require_dist.object({
|
|
35370
|
+
mode: MediaReclaimActionModeSchema,
|
|
35371
|
+
/** Which populations to walk. Omitted = both. */
|
|
35372
|
+
scopes: require_dist.array(MediaReclaimScopeSchema).min(1).optional(),
|
|
35373
|
+
/** Restrict to these cameras. Omitted = every camera in retention scope. */
|
|
35374
|
+
deviceIds: require_dist.array(require_dist.number().int()).min(1).optional(),
|
|
35375
|
+
/** Start every device from the top instead of resuming its cursor. */
|
|
35376
|
+
restart: require_dist.boolean().optional(),
|
|
35377
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
35378
|
+
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
35379
|
+
/** Rows EXAMINED per camera in this pass — the pacing bound. */
|
|
35380
|
+
maxRowsPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
35381
|
+
/** Rows DELETED per camera in this pass — the blast-radius bound. */
|
|
35382
|
+
maxReclaimPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
35383
|
+
/** Bytes released across the whole run — the disk-contention bound. */
|
|
35384
|
+
maxBytesPerRun: require_dist.number().int().min(1).optional(),
|
|
35385
|
+
/** Wall-clock ceiling for the run. */
|
|
35386
|
+
budgetMinutes: require_dist.number().int().min(1).max(720).optional(),
|
|
35387
|
+
/**
|
|
35388
|
+
* Bytes per second the pass may release. The recordings volume sustains
|
|
35389
|
+
* ~4 MB/s and the archive needs it more than this does; the default is half
|
|
35390
|
+
* of that.
|
|
35391
|
+
*/
|
|
35392
|
+
throttleBytesPerSec: require_dist.number().int().min(64 * 1024).optional(),
|
|
35393
|
+
/**
|
|
35394
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
35395
|
+
* exclusively save rows, never select them.
|
|
35396
|
+
*/
|
|
35397
|
+
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
35398
|
+
});
|
|
35399
|
+
var MediaReclaimDeviceReportSchema = require_dist.object({
|
|
35400
|
+
deviceId: require_dist.number(),
|
|
35401
|
+
scope: MediaReclaimScopeSchema,
|
|
35402
|
+
examined: require_dist.number(),
|
|
35403
|
+
eligible: require_dist.number(),
|
|
35404
|
+
reclaimed: require_dist.number(),
|
|
35405
|
+
bytesReclaimed: require_dist.number(),
|
|
35406
|
+
/** Rows kept because the owning track is pinned (`staging`/`favourited`). */
|
|
35407
|
+
held: require_dist.number(),
|
|
35408
|
+
/** Rows kept because the owner could not be shown to be a motion event. */
|
|
35409
|
+
unresolved: require_dist.number(),
|
|
35410
|
+
withinGrace: require_dist.number(),
|
|
35411
|
+
/** Deletes that REFUSED — the blob survived or the volume could not answer.
|
|
35412
|
+
* The row was kept and the cursor did not pass it. */
|
|
35413
|
+
refused: require_dist.number(),
|
|
35414
|
+
stoppedBy: require_dist.string(),
|
|
35415
|
+
/** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
|
|
35416
|
+
nextCursor: require_dist.number()
|
|
35417
|
+
});
|
|
35418
|
+
var MediaReclaimActionResultSchema = require_dist.object({
|
|
35419
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
35420
|
+
ran: require_dist.boolean(),
|
|
35421
|
+
mode: require_dist._enum(["report", "reclaim"]),
|
|
35422
|
+
devices: require_dist.array(MediaReclaimDeviceReportSchema),
|
|
35423
|
+
totalExamined: require_dist.number(),
|
|
35424
|
+
totalEligible: require_dist.number(),
|
|
35425
|
+
totalReclaimed: require_dist.number(),
|
|
35426
|
+
totalBytesReclaimed: require_dist.number(),
|
|
35427
|
+
totalRefused: require_dist.number(),
|
|
35428
|
+
durationMs: require_dist.number(),
|
|
35429
|
+
/** Every device+scope reported `exhausted` — there is nothing left to do. */
|
|
35430
|
+
complete: require_dist.boolean()
|
|
35431
|
+
});
|
|
35432
|
+
var mediaReclaimActions = require_dist.defineCustomActions({ "retention.mediaReclaim": require_dist.customAction(MediaReclaimActionInputSchema, MediaReclaimActionResultSchema, { auth: "admin" }) });
|
|
35433
|
+
//#endregion
|
|
35338
35434
|
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
35339
35435
|
/**
|
|
35340
35436
|
* The operator button for the orphan audit.
|
|
@@ -36691,6 +36787,28 @@ function trackMediaScopesForKinds(trackId, kinds) {
|
|
|
36691
36787
|
return kinds.some((kind) => DETAIL_CROP_KINDS.includes(kind)) ? scopes : scopes.filter((scope) => scope.publishCropAs === null);
|
|
36692
36788
|
}
|
|
36693
36789
|
/**
|
|
36790
|
+
* The STORED kinds worth reading from one scope for a `kinds` request, or
|
|
36791
|
+
* `null` when the scope cannot contribute to it at all.
|
|
36792
|
+
*
|
|
36793
|
+
* This is the same vocabulary translation {@link collectTrackMedia} performs on
|
|
36794
|
+
* the way out, moved to the way IN so it can narrow the query instead of the
|
|
36795
|
+
* answer. A caller naming `faceCrop` must ask the face owner for its stored
|
|
36796
|
+
* `crop` — asking it for `faceCrop` matches nothing, and asking it for
|
|
36797
|
+
* everything opens blobs the answer will discard.
|
|
36798
|
+
*
|
|
36799
|
+
* `undefined` means "every kind": the honest translation of an unfiltered
|
|
36800
|
+
* request, and the only safe answer for the open-ended track scope.
|
|
36801
|
+
*
|
|
36802
|
+
* `null` (skip) is only ever returned for a DETAIL owner whose one publishable
|
|
36803
|
+
* kind was not asked for. The track scope is never skipped — its kinds are
|
|
36804
|
+
* open-ended, and dropping it on a guess is how a listing starts lying.
|
|
36805
|
+
*/
|
|
36806
|
+
function storedKindsForScope(scope, kinds) {
|
|
36807
|
+
if (kinds === void 0 || kinds.length === 0) return void 0;
|
|
36808
|
+
if (scope.publishCropAs === null) return kinds;
|
|
36809
|
+
return kinds.includes(scope.publishCropAs) ? [STORED_DETAIL_CROP_KIND] : null;
|
|
36810
|
+
}
|
|
36811
|
+
/**
|
|
36694
36812
|
* Fold every scope's rows into ONE track media listing: detail crops
|
|
36695
36813
|
* relabelled, everything filtered to `kinds`, ordered oldest first.
|
|
36696
36814
|
*
|
|
@@ -36827,8 +36945,10 @@ var AnalyticsQueryFacade = class {
|
|
|
36827
36945
|
return this.withMediaUrl(events);
|
|
36828
36946
|
}
|
|
36829
36947
|
async getEventMedia(input) {
|
|
36830
|
-
const
|
|
36831
|
-
|
|
36948
|
+
const store = this.deps.mediaStore();
|
|
36949
|
+
if (store === null) return [];
|
|
36950
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36951
|
+
return (await store.listByOwner("event", input.eventId, input.deviceId, input.kind === void 0 ? void 0 : [input.kind])).map((row) => toWireMediaFile(row, prefix));
|
|
36832
36952
|
}
|
|
36833
36953
|
/**
|
|
36834
36954
|
* A track's media WITH the bytes, restricted to `kinds` when given.
|
|
@@ -36841,14 +36961,36 @@ var AnalyticsQueryFacade = class {
|
|
|
36841
36961
|
async getTrackMedia(input) {
|
|
36842
36962
|
const store = this.deps.mediaStore();
|
|
36843
36963
|
if (store === null) return [];
|
|
36964
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36844
36965
|
const scopes = trackMediaScopesForKinds(input.trackId, input.kinds);
|
|
36845
|
-
return collectTrackMedia(await Promise.all(scopes.map(async (scope) =>
|
|
36846
|
-
scope,
|
|
36847
|
-
|
|
36848
|
-
|
|
36966
|
+
return collectTrackMedia(await Promise.all(scopes.map(async (scope) => {
|
|
36967
|
+
const stored = storedKindsForScope(scope, input.kinds);
|
|
36968
|
+
if (stored === null) return {
|
|
36969
|
+
scope,
|
|
36970
|
+
rows: []
|
|
36971
|
+
};
|
|
36972
|
+
return {
|
|
36973
|
+
scope,
|
|
36974
|
+
rows: await store.listByOwner(scope.ownerKind, scope.ownerId, input.deviceId, stored)
|
|
36975
|
+
};
|
|
36976
|
+
})), (row, kind) => ({
|
|
36849
36977
|
...row,
|
|
36850
36978
|
kind
|
|
36851
|
-
}), input.kinds);
|
|
36979
|
+
}), input.kinds).map((row) => toWireMediaFile(row, prefix));
|
|
36980
|
+
}
|
|
36981
|
+
/**
|
|
36982
|
+
* An event's media WITHOUT the bytes.
|
|
36983
|
+
*
|
|
36984
|
+
* Index rows only — it never opens a blob. The track detail modal unions
|
|
36985
|
+
* this with {@link listTrackMedia} to build its filmstrip and then fetches
|
|
36986
|
+
* each tile from the `event-media` plane by key, so the bytes this used to
|
|
36987
|
+
* carry were read, base64'd, shipped and discarded.
|
|
36988
|
+
*/
|
|
36989
|
+
async listEventMedia(input) {
|
|
36990
|
+
const store = this.deps.mediaStore();
|
|
36991
|
+
if (store === null) return [];
|
|
36992
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36993
|
+
return (await store.listInfoByOwner("event", input.eventId, input.deviceId)).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36852
36994
|
}
|
|
36853
36995
|
/**
|
|
36854
36996
|
* A track's media WITHOUT the bytes.
|
|
@@ -36866,13 +37008,14 @@ var AnalyticsQueryFacade = class {
|
|
|
36866
37008
|
async listTrackMedia(input) {
|
|
36867
37009
|
const store = this.deps.mediaStore();
|
|
36868
37010
|
if (store === null) return [];
|
|
37011
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36869
37012
|
return collectTrackMedia(await Promise.all(trackMediaOwnerScopes(input.trackId).map(async (scope) => ({
|
|
36870
37013
|
scope,
|
|
36871
37014
|
rows: await store.listInfoByOwner(scope.ownerKind, scope.ownerId, input.deviceId)
|
|
36872
37015
|
}))), (row, kind) => ({
|
|
36873
37016
|
...row,
|
|
36874
37017
|
kind
|
|
36875
|
-
}), void 0);
|
|
37018
|
+
}), void 0).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36876
37019
|
}
|
|
36877
37020
|
async pruneEventsBefore(input) {
|
|
36878
37021
|
const eventStore = this.deps.eventStore();
|
|
@@ -39317,6 +39460,31 @@ function createEventMediaHandler(deps) {
|
|
|
39317
39460
|
}
|
|
39318
39461
|
//#endregion
|
|
39319
39462
|
//#region src/pipeline-analytics/event-thumbnail-resolver.ts
|
|
39463
|
+
/**
|
|
39464
|
+
* Rank `candidates` with `pick`, then open ONE blob — the winner's.
|
|
39465
|
+
*
|
|
39466
|
+
* The ladders decide on `kind` alone, so the bytes are dead weight until the
|
|
39467
|
+
* choice is made. Handing them pre-read rows meant a tile that serves the
|
|
39468
|
+
* 31 KB `thumbnailSmall` first read the whole 3 597 219-byte set, including a
|
|
39469
|
+
* 2.8 MB `keyFrame` no list view has ever rendered.
|
|
39470
|
+
*
|
|
39471
|
+
* A winner whose blob is GONE is dropped and the ladder re-run on the rest.
|
|
39472
|
+
* That fall-through is not an extra: `listByOwner` used to provide it for free
|
|
39473
|
+
* by skipping unreadable rows before the ladder ever saw them, and without it
|
|
39474
|
+
* an index row that outlived its file would 404 a tile that used to render.
|
|
39475
|
+
* Bounded by the candidate count — each attempt removes one row.
|
|
39476
|
+
*/
|
|
39477
|
+
async function readWinningBlob(candidates, pick, readBlob) {
|
|
39478
|
+
let remaining = candidates;
|
|
39479
|
+
while (remaining.length > 0) {
|
|
39480
|
+
const winner = pick(remaining);
|
|
39481
|
+
if (winner === void 0) return null;
|
|
39482
|
+
const bytes = await readBlob(winner.key);
|
|
39483
|
+
if (bytes !== null) return bytes;
|
|
39484
|
+
remaining = remaining.filter((row) => row.key !== winner.key);
|
|
39485
|
+
}
|
|
39486
|
+
return null;
|
|
39487
|
+
}
|
|
39320
39488
|
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
39321
39489
|
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
39322
39490
|
* for new tracks (served only for old stored rows). Undefined when the event
|
|
@@ -39367,15 +39535,24 @@ async function collectCropForcedMedia(source, id) {
|
|
|
39367
39535
|
};
|
|
39368
39536
|
}
|
|
39369
39537
|
/**
|
|
39370
|
-
*
|
|
39371
|
-
* has any, else the owning track's
|
|
39372
|
-
*
|
|
39538
|
+
* Which rows the default path should serve from, and how to rank them: the
|
|
39539
|
+
* event's OWN media when it has any, else the owning track's.
|
|
39540
|
+
*
|
|
39541
|
+
* It yields the candidate SET rather than a winner so the caller can open one
|
|
39542
|
+
* blob and, if that blob has gone, re-rank the rest ({@link readWinningBlob}).
|
|
39543
|
+
* Returning a single row would force the caller to read it blind and 404 a tile
|
|
39544
|
+
* whose index row outlived its file.
|
|
39373
39545
|
*/
|
|
39374
|
-
async function
|
|
39375
|
-
|
|
39376
|
-
|
|
39546
|
+
async function collectDefaultEventMedia(deps) {
|
|
39547
|
+
if (deps.eventFiles.length > 0) return {
|
|
39548
|
+
candidates: deps.eventFiles,
|
|
39549
|
+
pick: pickEventOwnedMedia
|
|
39550
|
+
};
|
|
39377
39551
|
const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
|
|
39378
|
-
return
|
|
39552
|
+
return {
|
|
39553
|
+
candidates: await deps.listTrackMedia(trackId),
|
|
39554
|
+
pick: pickTrackFallbackMedia
|
|
39555
|
+
};
|
|
39379
39556
|
}
|
|
39380
39557
|
//#endregion
|
|
39381
39558
|
//#region src/pipeline-analytics/face-settings.ts
|
|
@@ -47483,6 +47660,70 @@ var PackageDropDetector = class {
|
|
|
47483
47660
|
}
|
|
47484
47661
|
};
|
|
47485
47662
|
//#endregion
|
|
47663
|
+
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
47664
|
+
/**
|
|
47665
|
+
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
47666
|
+
*
|
|
47667
|
+
* 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
|
|
47668
|
+
* routinely pokes a little outside the vehicle box, and requiring total
|
|
47669
|
+
* containment would count almost none of them. 0.8 and not 0.5: half a person
|
|
47670
|
+
* box overlapping a car is what a pedestrian walking in front of one looks
|
|
47671
|
+
* like, and counting those would make the number mean "somebody was near a
|
|
47672
|
+
* car", which is not a defect.
|
|
47673
|
+
*/
|
|
47674
|
+
var CONTAINMENT_THRESHOLD = .8;
|
|
47675
|
+
/**
|
|
47676
|
+
* The classes that count as a vehicle for this test.
|
|
47677
|
+
*
|
|
47678
|
+
* A local list rather than a shared taxonomy on purpose: it exists to describe
|
|
47679
|
+
* ONE measurement, and a shared enum would make a change here silently change
|
|
47680
|
+
* detection behaviour somewhere else.
|
|
47681
|
+
*/
|
|
47682
|
+
var VEHICLE_CLASSES$1 = [
|
|
47683
|
+
"car",
|
|
47684
|
+
"truck",
|
|
47685
|
+
"bus",
|
|
47686
|
+
"motorcycle",
|
|
47687
|
+
"vehicle"
|
|
47688
|
+
];
|
|
47689
|
+
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
47690
|
+
function intersectionArea(a, b) {
|
|
47691
|
+
const left = Math.max(a.x, b.x);
|
|
47692
|
+
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
47693
|
+
const top = Math.max(a.y, b.y);
|
|
47694
|
+
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
47695
|
+
if (right <= left || bottom <= top) return 0;
|
|
47696
|
+
return (right - left) * (bottom - top);
|
|
47697
|
+
}
|
|
47698
|
+
/**
|
|
47699
|
+
* Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
|
|
47700
|
+
*
|
|
47701
|
+
* `0` for a degenerate person box: a zero-area box is inside everything and
|
|
47702
|
+
* inside nothing, and answering `1` would count every one of them.
|
|
47703
|
+
*/
|
|
47704
|
+
function containedFraction(person, vehicle) {
|
|
47705
|
+
const area = person.w * person.h;
|
|
47706
|
+
if (area <= 0) return 0;
|
|
47707
|
+
return intersectionArea(person, vehicle) / area;
|
|
47708
|
+
}
|
|
47709
|
+
/**
|
|
47710
|
+
* Is this person box contained in any vehicle box in the same frame?
|
|
47711
|
+
*
|
|
47712
|
+
* Returns the fraction for the BEST-containing vehicle when it clears
|
|
47713
|
+
* {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
|
|
47714
|
+
* a counter that only ever says "yes" cannot later be re-thresholded against
|
|
47715
|
+
* what was actually measured.
|
|
47716
|
+
*/
|
|
47717
|
+
function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
|
|
47718
|
+
let best = 0;
|
|
47719
|
+
for (const neighbour of neighbours) {
|
|
47720
|
+
if (!VEHICLE_CLASSES$1.includes(neighbour.className)) continue;
|
|
47721
|
+
const fraction = containedFraction(person, neighbour.bbox);
|
|
47722
|
+
if (fraction > best) best = fraction;
|
|
47723
|
+
}
|
|
47724
|
+
return best >= threshold ? best : null;
|
|
47725
|
+
}
|
|
47726
|
+
//#endregion
|
|
47486
47727
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
|
|
47487
47728
|
/**
|
|
47488
47729
|
* stationary-evidence — how a parked object EARNS a registry entry.
|
|
@@ -50014,83 +50255,6 @@ function buildRecordedStillPorts(input) {
|
|
|
50014
50255
|
})
|
|
50015
50256
|
};
|
|
50016
50257
|
}
|
|
50017
|
-
function errorMessage(err) {
|
|
50018
|
-
return err instanceof Error ? err.message : String(err);
|
|
50019
|
-
}
|
|
50020
|
-
async function reconcileGallerySamples(deps, options) {
|
|
50021
|
-
let checked = 0;
|
|
50022
|
-
let reclaimed = 0;
|
|
50023
|
-
let keyless = 0;
|
|
50024
|
-
let skippedGrace = 0;
|
|
50025
|
-
let stoppedByCeiling = false;
|
|
50026
|
-
const identities = await deps.listIdentities();
|
|
50027
|
-
for (const identity of identities) {
|
|
50028
|
-
const samples = await deps.listSamples(identity.id);
|
|
50029
|
-
for (const sample of samples) {
|
|
50030
|
-
if (sample.mediaKey === void 0) {
|
|
50031
|
-
keyless += 1;
|
|
50032
|
-
continue;
|
|
50033
|
-
}
|
|
50034
|
-
if (options.now - sample.addedAt < options.graceMs) {
|
|
50035
|
-
skippedGrace += 1;
|
|
50036
|
-
continue;
|
|
50037
|
-
}
|
|
50038
|
-
if (reclaimed >= options.maxReclaimPerRun) {
|
|
50039
|
-
stoppedByCeiling = true;
|
|
50040
|
-
deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
|
|
50041
|
-
return {
|
|
50042
|
-
checked,
|
|
50043
|
-
reclaimed,
|
|
50044
|
-
keyless,
|
|
50045
|
-
skippedGrace,
|
|
50046
|
-
stoppedByCeiling
|
|
50047
|
-
};
|
|
50048
|
-
}
|
|
50049
|
-
let state;
|
|
50050
|
-
try {
|
|
50051
|
-
state = await deps.blobState(sample.mediaKey);
|
|
50052
|
-
} catch (err) {
|
|
50053
|
-
const reason = errorMessage(err);
|
|
50054
|
-
deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
|
|
50055
|
-
sampleId: sample.id,
|
|
50056
|
-
identity: identity.name,
|
|
50057
|
-
error: reason
|
|
50058
|
-
} });
|
|
50059
|
-
return {
|
|
50060
|
-
checked,
|
|
50061
|
-
reclaimed,
|
|
50062
|
-
keyless,
|
|
50063
|
-
skippedGrace,
|
|
50064
|
-
stoppedByCeiling,
|
|
50065
|
-
aborted: reason
|
|
50066
|
-
};
|
|
50067
|
-
}
|
|
50068
|
-
checked += 1;
|
|
50069
|
-
if (state === "exists") continue;
|
|
50070
|
-
await deps.removeSample(identity.id, sample.id);
|
|
50071
|
-
reclaimed += 1;
|
|
50072
|
-
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
50073
|
-
sampleId: sample.id,
|
|
50074
|
-
identity: identity.name,
|
|
50075
|
-
mediaKey: sample.mediaKey,
|
|
50076
|
-
state
|
|
50077
|
-
} });
|
|
50078
|
-
}
|
|
50079
|
-
}
|
|
50080
|
-
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
50081
|
-
checked,
|
|
50082
|
-
reclaimed,
|
|
50083
|
-
keyless,
|
|
50084
|
-
skippedGrace
|
|
50085
|
-
} });
|
|
50086
|
-
return {
|
|
50087
|
-
checked,
|
|
50088
|
-
reclaimed,
|
|
50089
|
-
keyless,
|
|
50090
|
-
skippedGrace,
|
|
50091
|
-
stoppedByCeiling
|
|
50092
|
-
};
|
|
50093
|
-
}
|
|
50094
50258
|
//#endregion
|
|
50095
50259
|
//#region src/pipeline-analytics/retention/embedding-orphan-scan.ts
|
|
50096
50260
|
/** The scope id this leg reports and persists its cursor under. */
|
|
@@ -50207,6 +50371,83 @@ async function scanEmbeddingOrphans(deps, limits, cursor) {
|
|
|
50207
50371
|
nextCursor: Math.max(0, offset)
|
|
50208
50372
|
};
|
|
50209
50373
|
}
|
|
50374
|
+
function errorMessage(err) {
|
|
50375
|
+
return err instanceof Error ? err.message : String(err);
|
|
50376
|
+
}
|
|
50377
|
+
async function reconcileGallerySamples(deps, options) {
|
|
50378
|
+
let checked = 0;
|
|
50379
|
+
let reclaimed = 0;
|
|
50380
|
+
let keyless = 0;
|
|
50381
|
+
let skippedGrace = 0;
|
|
50382
|
+
let stoppedByCeiling = false;
|
|
50383
|
+
const identities = await deps.listIdentities();
|
|
50384
|
+
for (const identity of identities) {
|
|
50385
|
+
const samples = await deps.listSamples(identity.id);
|
|
50386
|
+
for (const sample of samples) {
|
|
50387
|
+
if (sample.mediaKey === void 0) {
|
|
50388
|
+
keyless += 1;
|
|
50389
|
+
continue;
|
|
50390
|
+
}
|
|
50391
|
+
if (options.now - sample.addedAt < options.graceMs) {
|
|
50392
|
+
skippedGrace += 1;
|
|
50393
|
+
continue;
|
|
50394
|
+
}
|
|
50395
|
+
if (reclaimed >= options.maxReclaimPerRun) {
|
|
50396
|
+
stoppedByCeiling = true;
|
|
50397
|
+
deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
|
|
50398
|
+
return {
|
|
50399
|
+
checked,
|
|
50400
|
+
reclaimed,
|
|
50401
|
+
keyless,
|
|
50402
|
+
skippedGrace,
|
|
50403
|
+
stoppedByCeiling
|
|
50404
|
+
};
|
|
50405
|
+
}
|
|
50406
|
+
let state;
|
|
50407
|
+
try {
|
|
50408
|
+
state = await deps.blobState(sample.mediaKey);
|
|
50409
|
+
} catch (err) {
|
|
50410
|
+
const reason = errorMessage(err);
|
|
50411
|
+
deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
|
|
50412
|
+
sampleId: sample.id,
|
|
50413
|
+
identity: identity.name,
|
|
50414
|
+
error: reason
|
|
50415
|
+
} });
|
|
50416
|
+
return {
|
|
50417
|
+
checked,
|
|
50418
|
+
reclaimed,
|
|
50419
|
+
keyless,
|
|
50420
|
+
skippedGrace,
|
|
50421
|
+
stoppedByCeiling,
|
|
50422
|
+
aborted: reason
|
|
50423
|
+
};
|
|
50424
|
+
}
|
|
50425
|
+
checked += 1;
|
|
50426
|
+
if (state === "exists") continue;
|
|
50427
|
+
await deps.removeSample(identity.id, sample.id);
|
|
50428
|
+
reclaimed += 1;
|
|
50429
|
+
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
50430
|
+
sampleId: sample.id,
|
|
50431
|
+
identity: identity.name,
|
|
50432
|
+
mediaKey: sample.mediaKey,
|
|
50433
|
+
state
|
|
50434
|
+
} });
|
|
50435
|
+
}
|
|
50436
|
+
}
|
|
50437
|
+
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
50438
|
+
checked,
|
|
50439
|
+
reclaimed,
|
|
50440
|
+
keyless,
|
|
50441
|
+
skippedGrace
|
|
50442
|
+
} });
|
|
50443
|
+
return {
|
|
50444
|
+
checked,
|
|
50445
|
+
reclaimed,
|
|
50446
|
+
keyless,
|
|
50447
|
+
skippedGrace,
|
|
50448
|
+
stoppedByCeiling
|
|
50449
|
+
};
|
|
50450
|
+
}
|
|
50210
50451
|
/**
|
|
50211
50452
|
* Where the completion marker lives.
|
|
50212
50453
|
*
|
|
@@ -53269,14 +53510,14 @@ function isAfterCursor(row, cursor) {
|
|
|
53269
53510
|
if (row.lastSeen < cursor.lastSeen) return true;
|
|
53270
53511
|
return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
|
|
53271
53512
|
}
|
|
53272
|
-
var VEHICLE_CLASSES
|
|
53513
|
+
var VEHICLE_CLASSES = new Set([
|
|
53273
53514
|
"vehicle",
|
|
53274
53515
|
"car",
|
|
53275
53516
|
"truck",
|
|
53276
53517
|
"bus"
|
|
53277
53518
|
]);
|
|
53278
53519
|
function motionlessMaxPxForClass(className) {
|
|
53279
|
-
return VEHICLE_CLASSES
|
|
53520
|
+
return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
|
|
53280
53521
|
}
|
|
53281
53522
|
function netDisplacementPx(positions) {
|
|
53282
53523
|
const first = positions[0];
|
|
@@ -57036,70 +57277,6 @@ function hasWrappedDetection(entries) {
|
|
|
57036
57277
|
return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
|
|
57037
57278
|
}
|
|
57038
57279
|
//#endregion
|
|
57039
|
-
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
57040
|
-
/**
|
|
57041
|
-
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
57042
|
-
*
|
|
57043
|
-
* 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
|
|
57044
|
-
* routinely pokes a little outside the vehicle box, and requiring total
|
|
57045
|
-
* containment would count almost none of them. 0.8 and not 0.5: half a person
|
|
57046
|
-
* box overlapping a car is what a pedestrian walking in front of one looks
|
|
57047
|
-
* like, and counting those would make the number mean "somebody was near a
|
|
57048
|
-
* car", which is not a defect.
|
|
57049
|
-
*/
|
|
57050
|
-
var CONTAINMENT_THRESHOLD = .8;
|
|
57051
|
-
/**
|
|
57052
|
-
* The classes that count as a vehicle for this test.
|
|
57053
|
-
*
|
|
57054
|
-
* A local list rather than a shared taxonomy on purpose: it exists to describe
|
|
57055
|
-
* ONE measurement, and a shared enum would make a change here silently change
|
|
57056
|
-
* detection behaviour somewhere else.
|
|
57057
|
-
*/
|
|
57058
|
-
var VEHICLE_CLASSES = [
|
|
57059
|
-
"car",
|
|
57060
|
-
"truck",
|
|
57061
|
-
"bus",
|
|
57062
|
-
"motorcycle",
|
|
57063
|
-
"vehicle"
|
|
57064
|
-
];
|
|
57065
|
-
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
57066
|
-
function intersectionArea(a, b) {
|
|
57067
|
-
const left = Math.max(a.x, b.x);
|
|
57068
|
-
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
57069
|
-
const top = Math.max(a.y, b.y);
|
|
57070
|
-
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
57071
|
-
if (right <= left || bottom <= top) return 0;
|
|
57072
|
-
return (right - left) * (bottom - top);
|
|
57073
|
-
}
|
|
57074
|
-
/**
|
|
57075
|
-
* Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
|
|
57076
|
-
*
|
|
57077
|
-
* `0` for a degenerate person box: a zero-area box is inside everything and
|
|
57078
|
-
* inside nothing, and answering `1` would count every one of them.
|
|
57079
|
-
*/
|
|
57080
|
-
function containedFraction(person, vehicle) {
|
|
57081
|
-
const area = person.w * person.h;
|
|
57082
|
-
if (area <= 0) return 0;
|
|
57083
|
-
return intersectionArea(person, vehicle) / area;
|
|
57084
|
-
}
|
|
57085
|
-
/**
|
|
57086
|
-
* Is this person box contained in any vehicle box in the same frame?
|
|
57087
|
-
*
|
|
57088
|
-
* Returns the fraction for the BEST-containing vehicle when it clears
|
|
57089
|
-
* {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
|
|
57090
|
-
* a counter that only ever says "yes" cannot later be re-thresholded against
|
|
57091
|
-
* what was actually measured.
|
|
57092
|
-
*/
|
|
57093
|
-
function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
|
|
57094
|
-
let best = 0;
|
|
57095
|
-
for (const neighbour of neighbours) {
|
|
57096
|
-
if (!VEHICLE_CLASSES.includes(neighbour.className)) continue;
|
|
57097
|
-
const fraction = containedFraction(person, neighbour.bbox);
|
|
57098
|
-
if (fraction > best) best = fraction;
|
|
57099
|
-
}
|
|
57100
|
-
return best >= threshold ? best : null;
|
|
57101
|
-
}
|
|
57102
|
-
//#endregion
|
|
57103
57280
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
57104
57281
|
function createCaptureCrop(deps) {
|
|
57105
57282
|
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
@@ -63541,6 +63718,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63541
63718
|
* Set once the data-plane is registered; null until then (e.g. no
|
|
63542
63719
|
* dataPlane facility in the current environment). */
|
|
63543
63720
|
eventMediaBaseUrl = null;
|
|
63721
|
+
/**
|
|
63722
|
+
* The SAME path, derived rather than observed — a pure function of the addon
|
|
63723
|
+
* id, so it exists before `serve()` is attempted and survives its failure.
|
|
63724
|
+
*
|
|
63725
|
+
* `eventMediaBaseUrl` above answers "is the plane up?", which is the right
|
|
63726
|
+
* question for the OPTIONAL `mediaUrl` stamp on a slim event projection.
|
|
63727
|
+
* It is the wrong question for a media row's `url`, which is that row's only
|
|
63728
|
+
* address: a row that dropped its address on a boot race would be
|
|
63729
|
+
* indistinguishable from a row with no bytes, and this repo has already paid
|
|
63730
|
+
* for one read that quietly returned less than it was asked for.
|
|
63731
|
+
*/
|
|
63732
|
+
eventMediaPathPrefix() {
|
|
63733
|
+
return `/addon/${this.ctx.id}/event-media`;
|
|
63734
|
+
}
|
|
63544
63735
|
lastActiveTrackIds = /* @__PURE__ */ new Map();
|
|
63545
63736
|
/** See `pipeline/suppressed-births.ts` — rejected births must not be
|
|
63546
63737
|
* re-upserted, and must be forgotten when the tracker drops the id. */
|
|
@@ -63791,6 +63982,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63791
63982
|
sensorEventStore: () => this.sensorEventStore,
|
|
63792
63983
|
opsLog: () => this.eventsOpsLog,
|
|
63793
63984
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
63985
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
63794
63986
|
logger: () => this.ctx.logger,
|
|
63795
63987
|
zoneCatalogue: () => this.zoneCatalogue
|
|
63796
63988
|
});
|
|
@@ -66333,6 +66525,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66333
66525
|
logger: this.ctx.logger,
|
|
66334
66526
|
refreshGallery: () => this.faceRecognizer?.refreshGallery(),
|
|
66335
66527
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66528
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66336
66529
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload)
|
|
66337
66530
|
});
|
|
66338
66531
|
this.faceGalleryProvider = faceGallery;
|
|
@@ -66347,6 +66540,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66347
66540
|
this.plateRecognizer?.refreshGallery();
|
|
66348
66541
|
},
|
|
66349
66542
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66543
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66350
66544
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
|
|
66351
66545
|
});
|
|
66352
66546
|
return [
|
|
@@ -70032,6 +70226,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70032
70226
|
async listTrackMedia(input) {
|
|
70033
70227
|
return this.queryFacade.listTrackMedia(input);
|
|
70034
70228
|
}
|
|
70229
|
+
/** What media an EVENT has, without any of it — the same manifest, for the
|
|
70230
|
+
* half of the detail modal's filmstrip that is event-owned. */
|
|
70231
|
+
async listEventMedia(input) {
|
|
70232
|
+
return this.queryFacade.listEventMedia(input);
|
|
70233
|
+
}
|
|
70035
70234
|
/**
|
|
70036
70235
|
* Search object events by text using CLIP cosine similarity.
|
|
70037
70236
|
*
|
|
@@ -71752,31 +71951,44 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71752
71951
|
return media;
|
|
71753
71952
|
}
|
|
71754
71953
|
}
|
|
71954
|
+
/**
|
|
71955
|
+
* Resolve an event/track id to ONE tile's bytes.
|
|
71956
|
+
*
|
|
71957
|
+
* Every ladder below ranks rows by `kind` alone, so this walks the INDEX
|
|
71958
|
+
* (`listInfoByOwner`, no blob opened) and opens only the winner. It used to
|
|
71959
|
+
* walk `listByOwner`, which reads and base64-encodes every blob an owner
|
|
71960
|
+
* holds before the first comparison is made: measured on the live hub
|
|
71961
|
+
* 2026-08-30, a track's six kinds are 3 597 219 bytes and the tile serves
|
|
71962
|
+
* `thumbnailSmall` — 31 155 of them. An Events page of forty tiles read
|
|
71963
|
+
* ~144 MB and allocated ~190 MB of base64 to deliver ~1.2 MB, all of it on
|
|
71964
|
+
* the single-threaded post-analysis runner.
|
|
71965
|
+
*/
|
|
71755
71966
|
async readEventThumbnail(id, preferKind) {
|
|
71756
|
-
const
|
|
71967
|
+
const listInfo = (ownerKind, ownerId) => this.mediaStore?.listInfoByOwner(ownerKind, ownerId) ?? Promise.resolve([]);
|
|
71968
|
+
const getTrackIdForEvent = (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null);
|
|
71969
|
+
const readBlob = async (key) => {
|
|
71970
|
+
const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
|
|
71971
|
+
return file === null ? null : {
|
|
71972
|
+
bytes: Buffer.from(file.base64, "base64"),
|
|
71973
|
+
key: file.key
|
|
71974
|
+
};
|
|
71975
|
+
};
|
|
71976
|
+
const eventFiles = await listInfo("event", id);
|
|
71757
71977
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
71758
71978
|
const layers = await collectCropForcedMedia({
|
|
71759
|
-
listByOwner:
|
|
71760
|
-
getTrackIdForEvent
|
|
71979
|
+
listByOwner: listInfo,
|
|
71980
|
+
getTrackIdForEvent
|
|
71761
71981
|
}, id);
|
|
71762
|
-
const
|
|
71763
|
-
|
|
71764
|
-
return {
|
|
71765
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71766
|
-
key: chosen.key
|
|
71767
|
-
};
|
|
71982
|
+
const bodyKeys = new Set(layers.bodyFiles.map((f) => f.key));
|
|
71983
|
+
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);
|
|
71768
71984
|
}
|
|
71769
|
-
const
|
|
71985
|
+
const { candidates, pick } = await collectDefaultEventMedia({
|
|
71770
71986
|
eventId: id,
|
|
71771
71987
|
eventFiles,
|
|
71772
|
-
listTrackMedia: (trackId) =>
|
|
71773
|
-
getTrackIdForEvent
|
|
71988
|
+
listTrackMedia: (trackId) => listInfo("track", trackId),
|
|
71989
|
+
getTrackIdForEvent
|
|
71774
71990
|
});
|
|
71775
|
-
|
|
71776
|
-
return {
|
|
71777
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71778
|
-
key: chosen.key
|
|
71779
|
-
};
|
|
71991
|
+
return await readWinningBlob(candidates, pick, readBlob);
|
|
71780
71992
|
}
|
|
71781
71993
|
globalSettingsSchema(cap) {
|
|
71782
71994
|
return this.schema(buildGlobalSettingsSchema(cap));
|