@camstack/addon-post-analysis 1.2.151 → 1.2.153
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-B5K63Zmt.mjs → dist-DDIu0Q1W.mjs} +381 -194
- package/dist/{dist-DpQno5NK.js → dist-DeMgxNFC.js} +386 -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-Cil5ur25.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-CG8y8a6j.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-CgUMPlDe.mjs → hostInit-I37PqSoI.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +1055 -527
- package/dist/pipeline-analytics/index.mjs +1054 -526
- 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-DeMgxNFC.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
|
},
|
|
@@ -25660,6 +25694,67 @@ var MEDIA_INDEXES = [
|
|
|
25660
25694
|
{
|
|
25661
25695
|
name: "idx_media_location",
|
|
25662
25696
|
columns: ["locationId"]
|
|
25697
|
+
}),
|
|
25698
|
+
(
|
|
25699
|
+
/**
|
|
25700
|
+
* The events-management footprint — `GROUP BY deviceId` and
|
|
25701
|
+
* `SUM(sizeBytes) WHERE deviceId = ?`. See {@link MediaStore.footprintByDevice}.
|
|
25702
|
+
*
|
|
25703
|
+
* **Why not `idx_media_device_ts`.** That index already leads with
|
|
25704
|
+
* `deviceId`, so it can find the range — but it does not carry `sizeBytes`,
|
|
25705
|
+
* so the sum has to fetch the table row behind every index entry it visits.
|
|
25706
|
+
* Over the whole collection that is one scattered read of the entire ~1.25 M
|
|
25707
|
+
* row table (`path` alone is ~80 characters per row) on the volume the
|
|
25708
|
+
* recorder is writing to. With `sizeBytes` in the index the planner never
|
|
25709
|
+
* touches the table: `SCAN … USING COVERING INDEX idx_media_device_size`.
|
|
25710
|
+
*
|
|
25711
|
+
* It has to be a NEW index rather than a third column on the existing one.
|
|
25712
|
+
* `declareCollection` emits `CREATE INDEX IF NOT EXISTS` keyed on the index
|
|
25713
|
+
* NAME, so widening `idx_media_device_ts` in place would be a silent no-op on
|
|
25714
|
+
* every hub that already has it — the declaration would read as done and the
|
|
25715
|
+
* query would still hit the table.
|
|
25716
|
+
*
|
|
25717
|
+
* **What it costs.** ~19 bytes of index per row (two integers plus the
|
|
25718
|
+
* rowid), so ~24 MB at 1.25 M rows, plus one more b-tree maintained by every
|
|
25719
|
+
* insert into a table taking ~92 k rows/day. At ~1 insert/s that maintenance
|
|
25720
|
+
* is noise beside the JPEG write the same call performs. The cost that is not
|
|
25721
|
+
* noise is the one-off: the first `declareCollection` after this ships builds
|
|
25722
|
+
* the index over every existing row. Do not deploy it beside a running drain
|
|
25723
|
+
* or seal — the same caveat `idx_media_location` carries, for the same
|
|
25724
|
+
* reason.
|
|
25725
|
+
*/
|
|
25726
|
+
{
|
|
25727
|
+
name: "idx_media_device_size",
|
|
25728
|
+
columns: ["deviceId", "sizeBytes"]
|
|
25729
|
+
}),
|
|
25730
|
+
(
|
|
25731
|
+
/**
|
|
25732
|
+
* The per-KIND footprint — `COUNT(*)`/`SUM(sizeBytes) WHERE kind = ?`, and
|
|
25733
|
+
* the same scoped to one camera. See {@link MediaStore.footprintByKind}.
|
|
25734
|
+
*
|
|
25735
|
+
* `kind` leads no existing index, so without this each of the thirteen
|
|
25736
|
+
* per-kind questions is a full table scan and the breakdown costs thirteen of
|
|
25737
|
+
* them. With it, each kind is a contiguous covering range and the thirteen
|
|
25738
|
+
* together scan the index once — the cost of the whole breakdown is one pass
|
|
25739
|
+
* over ~36 MB of index instead of thirteen passes over the table.
|
|
25740
|
+
*
|
|
25741
|
+
* `deviceId` sits between `kind` and `sizeBytes` so the per-camera variant
|
|
25742
|
+
* (`WHERE kind = ? AND deviceId = ?`) stays covering too, for ~3 bytes a row
|
|
25743
|
+
* more than `(kind, sizeBytes)` would cost. Without it that query finds its
|
|
25744
|
+
* range from the index and then reads the table for every row in it.
|
|
25745
|
+
*
|
|
25746
|
+
* ~29 bytes of index per row, so ~36 MB at 1.25 M rows, and one more b-tree
|
|
25747
|
+
* on every insert. Same one-off caveat as the two indexes above: the first
|
|
25748
|
+
* declare builds it over every existing row, so do not ship it beside a
|
|
25749
|
+
* running drain or seal.
|
|
25750
|
+
*/
|
|
25751
|
+
{
|
|
25752
|
+
name: "idx_media_kind_device_size",
|
|
25753
|
+
columns: [
|
|
25754
|
+
"kind",
|
|
25755
|
+
"deviceId",
|
|
25756
|
+
"sizeBytes"
|
|
25757
|
+
]
|
|
25663
25758
|
})
|
|
25664
25759
|
];
|
|
25665
25760
|
/** The storage location of a media row/record: its stamped `locationId`, or
|
|
@@ -25746,7 +25841,7 @@ var MediaStore = class {
|
|
|
25746
25841
|
});
|
|
25747
25842
|
}
|
|
25748
25843
|
/** Persist a media blob and register its metadata row. Returns the
|
|
25749
|
-
* collection key (
|
|
25844
|
+
* collection key (MediaFileRef.key) that callers store as reference. */
|
|
25750
25845
|
async put(params) {
|
|
25751
25846
|
if (this.writeGate) return this.writeGate.run(() => this.putUnchecked(params));
|
|
25752
25847
|
return this.putUnchecked(params);
|
|
@@ -26068,8 +26163,20 @@ var MediaStore = class {
|
|
|
26068
26163
|
* its device at write time, so scoping the query by it makes a media read
|
|
26069
26164
|
* for a device the caller cannot name return nothing — the DB itself
|
|
26070
26165
|
* enforces the boundary, no post-hoc ownership check to forget.
|
|
26166
|
+
*
|
|
26167
|
+
* `kinds` is the WORK filter, and it belongs here rather than on the answer.
|
|
26168
|
+
* Every row this returns has had its blob read off disk and base64-encoded,
|
|
26169
|
+
* so a caller that filters afterwards has already paid for every kind it is
|
|
26170
|
+
* about to discard: measured on the live hub, one track's six kinds are
|
|
26171
|
+
* 3 597 219 bytes and `thumbnailSmall` is 31 155 of them, and asking for the
|
|
26172
|
+
* small one alone cost exactly as much as asking for all six. `kind` is a
|
|
26173
|
+
* real column (see {@link MEDIA_COLUMNS}), so the narrowing happens in the
|
|
26174
|
+
* query and the blobs of the other kinds are never opened.
|
|
26175
|
+
*
|
|
26176
|
+
* Absent (or empty) means every kind — the back-compat default, and the
|
|
26177
|
+
* honest answer for a caller that has not decided what it needs.
|
|
26071
26178
|
*/
|
|
26072
|
-
async listByOwner(ownerKind, ownerId, deviceId) {
|
|
26179
|
+
async listByOwner(ownerKind, ownerId, deviceId, kinds) {
|
|
26073
26180
|
const rows = await this.store.query.query({
|
|
26074
26181
|
collection: MEDIA_COLLECTION,
|
|
26075
26182
|
filter: {
|
|
@@ -26081,6 +26188,7 @@ var MediaStore = class {
|
|
|
26081
26188
|
ownerId,
|
|
26082
26189
|
deviceId
|
|
26083
26190
|
},
|
|
26191
|
+
...kinds !== void 0 && kinds.length > 0 ? { whereIn: { kind: [...kinds] } } : {},
|
|
26084
26192
|
orderBy: {
|
|
26085
26193
|
field: "timestamp",
|
|
26086
26194
|
direction: "asc"
|
|
@@ -26301,47 +26409,136 @@ var MediaStore = class {
|
|
|
26301
26409
|
};
|
|
26302
26410
|
}
|
|
26303
26411
|
/**
|
|
26304
|
-
* On-demand footprint aggregation for the events-management UI:
|
|
26305
|
-
* `sizeBytes`
|
|
26306
|
-
*
|
|
26307
|
-
*
|
|
26308
|
-
*
|
|
26412
|
+
* On-demand footprint aggregation for the events-management UI: rows and
|
|
26413
|
+
* summed `sizeBytes` per camera, across the whole media collection.
|
|
26414
|
+
*
|
|
26415
|
+
* ## Why this is a histogram plus one aggregate per camera
|
|
26416
|
+
*
|
|
26417
|
+
* It used to page the ENTIRE collection — `ORDER BY id LIMIT 1000 OFFSET n`,
|
|
26418
|
+
* no predicate — and add `sizeBytes` up in JS. SQLite honours an OFFSET by
|
|
26419
|
+
* stepping over `n` entries, so the walk is quadratic in the table: at the
|
|
26420
|
+
* live hub's 1.25 M rows that is ~1 250 round trips and ~780 M index steps,
|
|
26421
|
+
* with every row serialised across an RPC boundary and rebuilt in V8 to be
|
|
26422
|
+
* added to a number. It timed out at 60 s on every call it ever made, and it
|
|
26423
|
+
* is the only thing that can size the surplus-media deletion it gates.
|
|
26424
|
+
*
|
|
26425
|
+
* The answer is at most a few dozen `(deviceId, rows, bytes)` triples.
|
|
26426
|
+
*
|
|
26427
|
+
* - The DEVICE SET comes from a histogram on `deviceId` with `bucketSize: 1`
|
|
26428
|
+
* — `GROUP BY deviceId`, served from `idx_media_device_size` as a covering
|
|
26429
|
+
* scan, the same trick `TrackStore.listDeviceIds` already uses (D56).
|
|
26430
|
+
* `bucket` IS the deviceId: the column is INTEGER and the bucket
|
|
26431
|
+
* expression is `CAST((deviceId - 0) / 1 AS INTEGER)`.
|
|
26432
|
+
* - The NUMBERS come from one `aggregate` per camera — `COUNT(*)` and
|
|
26433
|
+
* `SUM(sizeBytes)` in a single statement over an indexed `deviceId` range.
|
|
26434
|
+
*
|
|
26435
|
+
* Both figures for a camera come from the SAME statement deliberately. Taking
|
|
26436
|
+
* `rows` from the histogram and `bytes` from the aggregate would let a write
|
|
26437
|
+
* landing between the two produce a footprint whose row count and byte count
|
|
26438
|
+
* describe different populations — a disagreement an operator sizing a
|
|
26439
|
+
* deletion has no way to detect.
|
|
26440
|
+
*
|
|
26441
|
+
* A read that fails THROWS. It must never degrade to a zero: "this camera
|
|
26442
|
+
* holds nothing" is precisely the claim a failed measurement must not be
|
|
26443
|
+
* allowed to make to something about to delete (D295, D312).
|
|
26309
26444
|
*/
|
|
26310
26445
|
async footprintByDevice() {
|
|
26311
|
-
const
|
|
26446
|
+
const buckets = await this.store.histogram.query({
|
|
26447
|
+
collection: MEDIA_COLLECTION,
|
|
26448
|
+
field: "deviceId",
|
|
26449
|
+
bucketSize: 1,
|
|
26450
|
+
origin: 0
|
|
26451
|
+
});
|
|
26312
26452
|
const out = /* @__PURE__ */ new Map();
|
|
26313
|
-
|
|
26314
|
-
|
|
26315
|
-
const
|
|
26453
|
+
for (const bucket of buckets) {
|
|
26454
|
+
if (bucket.count === 0 || !Number.isFinite(bucket.bucket)) continue;
|
|
26455
|
+
const deviceId = bucket.bucket;
|
|
26456
|
+
const agg = await this.store.aggregate.query({
|
|
26316
26457
|
collection: MEDIA_COLLECTION,
|
|
26317
|
-
|
|
26318
|
-
|
|
26319
|
-
|
|
26320
|
-
|
|
26321
|
-
|
|
26322
|
-
|
|
26323
|
-
|
|
26324
|
-
|
|
26458
|
+
fields: [{
|
|
26459
|
+
as: "bytes",
|
|
26460
|
+
field: "sizeBytes",
|
|
26461
|
+
op: "sum"
|
|
26462
|
+
}],
|
|
26463
|
+
filter: { where: { deviceId } }
|
|
26464
|
+
});
|
|
26465
|
+
if (agg.count === 0) continue;
|
|
26466
|
+
const bytes = agg.values["bytes"];
|
|
26467
|
+
out.set(deviceId, {
|
|
26468
|
+
bytes: bytes ?? 0,
|
|
26469
|
+
rows: agg.count
|
|
26325
26470
|
});
|
|
26326
|
-
if (rows.length === 0) break;
|
|
26327
|
-
for (const row of rows) {
|
|
26328
|
-
const data = row.data;
|
|
26329
|
-
const deviceId = Number(data["deviceId"]);
|
|
26330
|
-
const sizeBytes = Number(data["sizeBytes"]);
|
|
26331
|
-
if (!Number.isFinite(deviceId)) continue;
|
|
26332
|
-
const acc = out.get(deviceId) ?? {
|
|
26333
|
-
bytes: 0,
|
|
26334
|
-
rows: 0
|
|
26335
|
-
};
|
|
26336
|
-
acc.bytes += Number.isFinite(sizeBytes) ? sizeBytes : 0;
|
|
26337
|
-
acc.rows += 1;
|
|
26338
|
-
out.set(deviceId, acc);
|
|
26339
|
-
}
|
|
26340
|
-
if (rows.length < PAGE) break;
|
|
26341
|
-
offset += PAGE;
|
|
26342
26471
|
}
|
|
26343
26472
|
return out;
|
|
26344
26473
|
}
|
|
26474
|
+
/**
|
|
26475
|
+
* The media footprint broken down by KIND — fleet-wide, or for one camera.
|
|
26476
|
+
*
|
|
26477
|
+
* `footprintByDevice` answers "how much is where"; this answers "how much is
|
|
26478
|
+
* WHAT", which is the axis a deletion decision turns on. The deletable set
|
|
26479
|
+
* (the periodic `snapshot` filmstrip, the surplus per-edge motion stills) and
|
|
26480
|
+
* the keep set (`firstFrame`, rolling `lastFrame`, `thumbnail` /
|
|
26481
|
+
* `thumbnailSmall`, `keyFrame` / `keyFrameSmall`, the face and plate buffers,
|
|
26482
|
+
* gallery media, the CLIP `crop`) are told apart by `kind` and by nothing
|
|
26483
|
+
* else. A byte total cannot size a deletion; this can.
|
|
26484
|
+
*
|
|
26485
|
+
* One `aggregate` per kind, each an indexed covering range on
|
|
26486
|
+
* `idx_media_kind_device_size`, plus ONE unfiltered aggregate for the totals.
|
|
26487
|
+
*
|
|
26488
|
+
* That last statement is the point of the design and not a spare round trip.
|
|
26489
|
+
* `MediaFileKindEnum.options` is the closed set the writers use, but the
|
|
26490
|
+
* TABLE is not closed: a row written by a retired path, or by a version that
|
|
26491
|
+
* knew a kind this one does not, matches no per-kind query. Adding the kinds
|
|
26492
|
+
* up to produce the total would make such a row invisible and the denominator
|
|
26493
|
+
* quietly too small — the failure mode that matters most here, because the
|
|
26494
|
+
* caller is about to delete against it. So the total is measured
|
|
26495
|
+
* independently and the difference is REPORTED as `unaccounted*` rather than
|
|
26496
|
+
* being absorbed.
|
|
26497
|
+
*/
|
|
26498
|
+
async footprintByKind(deviceId) {
|
|
26499
|
+
const scope = deviceId === void 0 ? void 0 : { where: { deviceId } };
|
|
26500
|
+
const fields = [{
|
|
26501
|
+
as: "bytes",
|
|
26502
|
+
field: "sizeBytes",
|
|
26503
|
+
op: "sum"
|
|
26504
|
+
}];
|
|
26505
|
+
const totals = await this.store.aggregate.query({
|
|
26506
|
+
collection: MEDIA_COLLECTION,
|
|
26507
|
+
fields: [...fields],
|
|
26508
|
+
...scope === void 0 ? {} : { filter: scope }
|
|
26509
|
+
});
|
|
26510
|
+
const kinds = [];
|
|
26511
|
+
let countedRows = 0;
|
|
26512
|
+
let countedBytes = 0;
|
|
26513
|
+
for (const kind of require_dist.MediaFileKindEnum.options) {
|
|
26514
|
+
const agg = await this.store.aggregate.query({
|
|
26515
|
+
collection: MEDIA_COLLECTION,
|
|
26516
|
+
fields: [...fields],
|
|
26517
|
+
filter: { where: {
|
|
26518
|
+
kind,
|
|
26519
|
+
...deviceId === void 0 ? {} : { deviceId }
|
|
26520
|
+
} }
|
|
26521
|
+
});
|
|
26522
|
+
if (agg.count === 0) continue;
|
|
26523
|
+
const bytes = agg.values["bytes"] ?? 0;
|
|
26524
|
+
countedRows += agg.count;
|
|
26525
|
+
countedBytes += bytes;
|
|
26526
|
+
kinds.push({
|
|
26527
|
+
kind,
|
|
26528
|
+
rows: agg.count,
|
|
26529
|
+
bytes
|
|
26530
|
+
});
|
|
26531
|
+
}
|
|
26532
|
+
kinds.sort((a, b) => b.bytes - a.bytes);
|
|
26533
|
+
const totalBytes = totals.values["bytes"] ?? 0;
|
|
26534
|
+
return {
|
|
26535
|
+
totalRows: totals.count,
|
|
26536
|
+
totalBytes,
|
|
26537
|
+
kinds,
|
|
26538
|
+
unaccountedRows: totals.count - countedRows,
|
|
26539
|
+
unaccountedBytes: totalBytes - countedBytes
|
|
26540
|
+
};
|
|
26541
|
+
}
|
|
26345
26542
|
/** Move a single media entry from one owner to another.
|
|
26346
26543
|
* The new copy is written before the old one is removed.
|
|
26347
26544
|
* The old-entry deletion is best-effort: on failure a warning is logged
|
|
@@ -27997,6 +28194,63 @@ var embeddingActions = require_dist.defineCustomActions({
|
|
|
27997
28194
|
"embedding.reembedFacesStatus": require_dist.customAction(NoInputSchema$1, ReembedFacesStatusSchema)
|
|
27998
28195
|
});
|
|
27999
28196
|
//#endregion
|
|
28197
|
+
//#region src/pipeline-analytics/media-file-url.ts
|
|
28198
|
+
/**
|
|
28199
|
+
* The plane path for one stored media key.
|
|
28200
|
+
*
|
|
28201
|
+
* `prefix` is `/addon/<addonId>/event-media` — a pure function of the addon
|
|
28202
|
+
* id, so it exists whether or not `dataPlane.serve` succeeded. A row that
|
|
28203
|
+
* named no address at all would be the silent drop this repo keeps paying for:
|
|
28204
|
+
* a client cannot tell "no bytes" from "no plane", and renders neither.
|
|
28205
|
+
*/
|
|
28206
|
+
function mediaFileUrl(prefix, key) {
|
|
28207
|
+
return `${prefix}/${encodeURIComponent(key)}`;
|
|
28208
|
+
}
|
|
28209
|
+
/**
|
|
28210
|
+
* A stored row as the wire sees it: the ref, the address, and — transitionally
|
|
28211
|
+
* — the bytes.
|
|
28212
|
+
*
|
|
28213
|
+
* `base64` is copied through ONLY because the deployed viewer's track-detail
|
|
28214
|
+
* hero still requires the field to parse a row at all. See the `MediaFile`
|
|
28215
|
+
* docblock in `pipeline-analytics.cap.ts`; deleting the field is a one-line
|
|
28216
|
+
* change here and one in the schema.
|
|
28217
|
+
*
|
|
28218
|
+
* `provenance` is deliberately NOT spread: it is store-internal, and the
|
|
28219
|
+
* previous `{ ...row, kind }` shape published it by accident.
|
|
28220
|
+
*/
|
|
28221
|
+
function toWireMediaFile(row, prefix) {
|
|
28222
|
+
return {
|
|
28223
|
+
key: row.key,
|
|
28224
|
+
kind: row.kind,
|
|
28225
|
+
sizeBytes: row.sizeBytes,
|
|
28226
|
+
timestamp: row.timestamp,
|
|
28227
|
+
url: mediaFileUrl(prefix, row.key),
|
|
28228
|
+
base64: row.base64
|
|
28229
|
+
};
|
|
28230
|
+
}
|
|
28231
|
+
/** The same row without its bytes — the manifest shape. Carries the SAME
|
|
28232
|
+
* `url` as the byte read, so a client never builds the plane path itself. */
|
|
28233
|
+
function toWireMediaFileInfo(row, prefix) {
|
|
28234
|
+
return {
|
|
28235
|
+
key: row.key,
|
|
28236
|
+
kind: row.kind,
|
|
28237
|
+
sizeBytes: row.sizeBytes,
|
|
28238
|
+
timestamp: row.timestamp,
|
|
28239
|
+
url: mediaFileUrl(prefix, row.key)
|
|
28240
|
+
};
|
|
28241
|
+
}
|
|
28242
|
+
//#endregion
|
|
28243
|
+
//#region src/pipeline-analytics/gallery-media-row.ts
|
|
28244
|
+
function toGalleryMediaRow(row, prefix) {
|
|
28245
|
+
return {
|
|
28246
|
+
key: row.key,
|
|
28247
|
+
kind: row.kind,
|
|
28248
|
+
url: mediaFileUrl(prefix, row.key),
|
|
28249
|
+
sizeBytes: row.sizeBytes,
|
|
28250
|
+
timestamp: row.timestamp
|
|
28251
|
+
};
|
|
28252
|
+
}
|
|
28253
|
+
//#endregion
|
|
28000
28254
|
//#region src/pipeline-analytics/include-crops-default.ts
|
|
28001
28255
|
function resolveIncludeCrops(input) {
|
|
28002
28256
|
return input?.includeCrops === true;
|
|
@@ -29656,6 +29910,7 @@ var FaceGalleryProvider = class {
|
|
|
29656
29910
|
logger;
|
|
29657
29911
|
refreshGallery;
|
|
29658
29912
|
eventMediaBaseUrl;
|
|
29913
|
+
eventMediaPathPrefix;
|
|
29659
29914
|
emitFaceGalleryChanged;
|
|
29660
29915
|
constructor(deps) {
|
|
29661
29916
|
this.identityStore = deps.identityStore;
|
|
@@ -29666,6 +29921,7 @@ var FaceGalleryProvider = class {
|
|
|
29666
29921
|
this.logger = deps.logger;
|
|
29667
29922
|
this.refreshGallery = deps.refreshGallery;
|
|
29668
29923
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
29924
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
29669
29925
|
this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
|
|
29670
29926
|
}
|
|
29671
29927
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
@@ -29853,13 +30109,7 @@ var FaceGalleryProvider = class {
|
|
|
29853
30109
|
* the enrolled identity copies — those live under identity owner).
|
|
29854
30110
|
*/
|
|
29855
30111
|
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
|
-
}));
|
|
30112
|
+
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
29863
30113
|
}
|
|
29864
30114
|
/**
|
|
29865
30115
|
* Assign a buffered face to an identity:
|
|
@@ -32572,50 +32822,136 @@ var FrameProcessor = class {
|
|
|
32572
32822
|
};
|
|
32573
32823
|
}
|
|
32574
32824
|
};
|
|
32575
|
-
|
|
32576
|
-
|
|
32825
|
+
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
32826
|
+
* evicts the oldest record. */
|
|
32827
|
+
var PendingPreviewUpgrades = class {
|
|
32828
|
+
entries = /* @__PURE__ */ new Map();
|
|
32829
|
+
ttlMs;
|
|
32830
|
+
maxEntries;
|
|
32831
|
+
now;
|
|
32832
|
+
constructor(options = {}) {
|
|
32833
|
+
this.ttlMs = options.ttlMs ?? 9e5;
|
|
32834
|
+
this.maxEntries = Math.max(1, options.maxEntries ?? 512);
|
|
32835
|
+
this.now = options.now ?? (() => Date.now());
|
|
32836
|
+
}
|
|
32837
|
+
/** Record (or replace) a closed track's pending upgrade. Prunes expired
|
|
32838
|
+
* entries first, then evicts the oldest while over the cap. */
|
|
32839
|
+
record(entry) {
|
|
32840
|
+
this.prune();
|
|
32841
|
+
this.entries.delete(entry.trackId);
|
|
32842
|
+
this.entries.set(entry.trackId, entry);
|
|
32843
|
+
while (this.entries.size > this.maxEntries) {
|
|
32844
|
+
const oldest = this.entries.keys().next();
|
|
32845
|
+
if (oldest.done === true) break;
|
|
32846
|
+
this.entries.delete(oldest.value);
|
|
32847
|
+
}
|
|
32848
|
+
}
|
|
32849
|
+
/** Claim a track's pending upgrade — removes it, so a second landing (or a
|
|
32850
|
+
* concurrent racer) finds nothing and the tile is rewritten at most once. */
|
|
32851
|
+
take(trackId) {
|
|
32852
|
+
this.prune();
|
|
32853
|
+
const entry = this.entries.get(trackId);
|
|
32854
|
+
if (entry === void 0) return void 0;
|
|
32855
|
+
this.entries.delete(trackId);
|
|
32856
|
+
return entry;
|
|
32857
|
+
}
|
|
32858
|
+
get size() {
|
|
32859
|
+
return this.entries.size;
|
|
32860
|
+
}
|
|
32861
|
+
prune() {
|
|
32862
|
+
const cutoff = this.now() - this.ttlMs;
|
|
32863
|
+
for (const [trackId, entry] of this.entries) if (entry.recordedAtMs < cutoff) this.entries.delete(trackId);
|
|
32864
|
+
}
|
|
32865
|
+
};
|
|
32577
32866
|
/**
|
|
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.
|
|
32867
|
+
* Redeem a closed track's pending preview off a keyFrame that landed late.
|
|
32868
|
+
* Every non-`upgraded` outcome that had a pending entry logs itself — a
|
|
32869
|
+
* branch that leaves the stand-in in place must say so.
|
|
32583
32870
|
*/
|
|
32584
|
-
function
|
|
32585
|
-
const
|
|
32586
|
-
|
|
32587
|
-
if (
|
|
32588
|
-
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
|
|
32592
|
-
|
|
32593
|
-
frame
|
|
32871
|
+
async function runPreviewUpgrade(deps, input) {
|
|
32872
|
+
const { deviceId, trackId } = input;
|
|
32873
|
+
const pending = deps.takePending(trackId);
|
|
32874
|
+
if (pending === void 0) return "no-pending";
|
|
32875
|
+
const held = await deps.heldThumbnailProvenance(trackId);
|
|
32876
|
+
if (held !== null && mediaProvenanceRank(held) >= mediaProvenanceRank("native")) {
|
|
32877
|
+
deps.logger.info("late keyFrame: preview already native — no upgrade", {
|
|
32878
|
+
tags: { deviceId },
|
|
32879
|
+
meta: { trackId }
|
|
32594
32880
|
});
|
|
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;
|
|
32881
|
+
return "held-native";
|
|
32882
|
+
}
|
|
32883
|
+
if (pending.bestSeen === void 0) {
|
|
32884
|
+
deps.logger.info("late keyFrame landed but no subject bbox was ever seen — tile kept as-is", {
|
|
32885
|
+
tags: { deviceId },
|
|
32886
|
+
meta: {
|
|
32887
|
+
trackId,
|
|
32888
|
+
heldProvenance: held
|
|
32609
32889
|
}
|
|
32610
|
-
}
|
|
32890
|
+
});
|
|
32891
|
+
return "no-bbox";
|
|
32611
32892
|
}
|
|
32612
|
-
|
|
32613
|
-
|
|
32614
|
-
|
|
32615
|
-
|
|
32616
|
-
|
|
32617
|
-
|
|
32618
|
-
|
|
32893
|
+
if (pending.bestSeen.timestamp === void 0) {
|
|
32894
|
+
deps.logger.info("late keyFrame: pending entry predates the motion guard — tile kept as-is", {
|
|
32895
|
+
tags: { deviceId },
|
|
32896
|
+
meta: {
|
|
32897
|
+
trackId,
|
|
32898
|
+
keyFrameKey: input.keyFrameKey,
|
|
32899
|
+
heldProvenance: held
|
|
32900
|
+
}
|
|
32901
|
+
});
|
|
32902
|
+
return "no-bbox";
|
|
32903
|
+
}
|
|
32904
|
+
const derived = await deps.derive({
|
|
32905
|
+
deviceId,
|
|
32906
|
+
mediaKey: input.keyFrameKey,
|
|
32907
|
+
bbox: {
|
|
32908
|
+
x: pending.bestSeen.x,
|
|
32909
|
+
y: pending.bestSeen.y,
|
|
32910
|
+
w: pending.bestSeen.w,
|
|
32911
|
+
h: pending.bestSeen.h
|
|
32912
|
+
},
|
|
32913
|
+
frameWidth: pending.bestSeen.frameWidth,
|
|
32914
|
+
frameHeight: pending.bestSeen.frameHeight,
|
|
32915
|
+
timestamp: pending.bestSeen.timestamp
|
|
32916
|
+
});
|
|
32917
|
+
if (derived === null) {
|
|
32918
|
+
deps.logger.info("late keyFrame preview upgrade failed (derive miss) — tile kept as-is", {
|
|
32919
|
+
tags: { deviceId },
|
|
32920
|
+
meta: {
|
|
32921
|
+
trackId,
|
|
32922
|
+
keyFrameKey: input.keyFrameKey,
|
|
32923
|
+
heldProvenance: held
|
|
32924
|
+
}
|
|
32925
|
+
});
|
|
32926
|
+
return "derive-miss";
|
|
32927
|
+
}
|
|
32928
|
+
await deps.put({
|
|
32929
|
+
deviceId,
|
|
32930
|
+
trackId,
|
|
32931
|
+
kind: "thumbnail",
|
|
32932
|
+
timestamp: input.timestamp,
|
|
32933
|
+
data: derived.thumbnail,
|
|
32934
|
+
provenance: "native"
|
|
32935
|
+
});
|
|
32936
|
+
await deps.put({
|
|
32937
|
+
deviceId,
|
|
32938
|
+
trackId,
|
|
32939
|
+
kind: "thumbnailSmall",
|
|
32940
|
+
timestamp: input.timestamp,
|
|
32941
|
+
data: derived.thumbnailSmall,
|
|
32942
|
+
provenance: "native"
|
|
32943
|
+
});
|
|
32944
|
+
await deps.clearPreviewMiss(deviceId, trackId);
|
|
32945
|
+
deps.logger.info("best-shot upgraded from late keyFrame (stand-in replaced)", {
|
|
32946
|
+
tags: { deviceId },
|
|
32947
|
+
meta: {
|
|
32948
|
+
trackId,
|
|
32949
|
+
keyFrameKey: input.keyFrameKey,
|
|
32950
|
+
replaced: held ?? "none",
|
|
32951
|
+
skewMs: derived.skewMs
|
|
32952
|
+
}
|
|
32953
|
+
});
|
|
32954
|
+
return "upgraded";
|
|
32619
32955
|
}
|
|
32620
32956
|
function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, score) {
|
|
32621
32957
|
if (frameWidth <= 0 || frameHeight <= 0) return true;
|
|
@@ -32775,137 +33111,6 @@ function promoteSemanticBestFrame(deps, input) {
|
|
|
32775
33111
|
captured: true
|
|
32776
33112
|
};
|
|
32777
33113
|
}
|
|
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
33114
|
//#endregion
|
|
32910
33115
|
//#region src/pipeline-analytics/store/zone-geometry.ts
|
|
32911
33116
|
/**
|
|
@@ -34644,6 +34849,7 @@ var PlateGalleryProvider = class {
|
|
|
34644
34849
|
refreshGallery;
|
|
34645
34850
|
emitPlateGalleryChanged;
|
|
34646
34851
|
eventMediaBaseUrl;
|
|
34852
|
+
eventMediaPathPrefix;
|
|
34647
34853
|
constructor(deps) {
|
|
34648
34854
|
this.plateStore = deps.plateStore;
|
|
34649
34855
|
this.vehicleStore = deps.vehicleStore;
|
|
@@ -34654,6 +34860,7 @@ var PlateGalleryProvider = class {
|
|
|
34654
34860
|
this.refreshGallery = deps.refreshGallery;
|
|
34655
34861
|
this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
|
|
34656
34862
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
34863
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
34657
34864
|
}
|
|
34658
34865
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
34659
34866
|
safeEmitPlateGalleryChanged(payload) {
|
|
@@ -34724,13 +34931,7 @@ var PlateGalleryProvider = class {
|
|
|
34724
34931
|
return this.toPlateInfo(plate, nameMap, { includeCrops: false });
|
|
34725
34932
|
}
|
|
34726
34933
|
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
|
-
}));
|
|
34934
|
+
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
34734
34935
|
}
|
|
34735
34936
|
async searchPlates(input) {
|
|
34736
34937
|
const maxDistance = input.maxDistance ?? 1;
|
|
@@ -34973,6 +35174,51 @@ var PlateGalleryProvider = class {
|
|
|
34973
35174
|
}
|
|
34974
35175
|
};
|
|
34975
35176
|
//#endregion
|
|
35177
|
+
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
35178
|
+
/**
|
|
35179
|
+
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
35180
|
+
* return the tracks it produced.
|
|
35181
|
+
*
|
|
35182
|
+
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
35183
|
+
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
35184
|
+
*/
|
|
35185
|
+
function runFrameProcessorPass(input) {
|
|
35186
|
+
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
35187
|
+
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
35188
|
+
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
35189
|
+
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
35190
|
+
const accum = /* @__PURE__ */ new Map();
|
|
35191
|
+
for (const { timestamp, frame } of input.frames) {
|
|
35192
|
+
const result = processor.process({
|
|
35193
|
+
timestamp,
|
|
35194
|
+
frame
|
|
35195
|
+
});
|
|
35196
|
+
for (const td of result.tracked) {
|
|
35197
|
+
const existing = accum.get(td.trackId);
|
|
35198
|
+
const matched = td.matchedThisFrame ? 1 : 0;
|
|
35199
|
+
if (existing === void 0) accum.set(td.trackId, {
|
|
35200
|
+
className: td.className,
|
|
35201
|
+
firstSeenMs: timestamp,
|
|
35202
|
+
lastSeenMs: timestamp,
|
|
35203
|
+
bbox: { ...td.bbox },
|
|
35204
|
+
framesMatched: matched
|
|
35205
|
+
});
|
|
35206
|
+
else {
|
|
35207
|
+
existing.className = td.className;
|
|
35208
|
+
existing.lastSeenMs = timestamp;
|
|
35209
|
+
existing.framesMatched += matched;
|
|
35210
|
+
}
|
|
35211
|
+
}
|
|
35212
|
+
}
|
|
35213
|
+
return { tracks: [...accum.values()].map((t) => ({
|
|
35214
|
+
className: t.className,
|
|
35215
|
+
firstSeenMs: t.firstSeenMs,
|
|
35216
|
+
lastSeenMs: t.lastSeenMs,
|
|
35217
|
+
bbox: t.bbox,
|
|
35218
|
+
framesMatched: t.framesMatched
|
|
35219
|
+
})) };
|
|
35220
|
+
}
|
|
35221
|
+
//#endregion
|
|
34976
35222
|
//#region src/pipeline-analytics/retention/disk-reconcile.ts
|
|
34977
35223
|
/**
|
|
34978
35224
|
* Disk-wins reconcile for pipeline analytics.
|
|
@@ -35037,93 +35283,6 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
35037
35283
|
};
|
|
35038
35284
|
}
|
|
35039
35285
|
//#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
35286
|
//#region src/pipeline-analytics/retention/media-reclaim.ts
|
|
35128
35287
|
var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
|
|
35129
35288
|
var DEFAULT_MEDIA_RECLAIM_LIMITS = {
|
|
@@ -35335,6 +35494,93 @@ async function runMediaReclaim(input) {
|
|
|
35335
35494
|
};
|
|
35336
35495
|
}
|
|
35337
35496
|
//#endregion
|
|
35497
|
+
//#region src/pipeline-analytics/retention/media-reclaim-actions.ts
|
|
35498
|
+
/**
|
|
35499
|
+
* The operator button for the debug-media reclaim.
|
|
35500
|
+
*
|
|
35501
|
+
* The mechanism ships armed with nothing: **arming it is the operator's
|
|
35502
|
+
* call**. `mode` omitted means `report` — it counts, logs one line per camera
|
|
35503
|
+
* and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
|
|
35504
|
+
* that has to be typed.
|
|
35505
|
+
*
|
|
35506
|
+
* Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
|
|
35507
|
+
* earned it: dry-run default, explicit pacing, explicit blast radius, and a
|
|
35508
|
+
* report you can read instead of a log line you have to find.
|
|
35509
|
+
*/
|
|
35510
|
+
/**
|
|
35511
|
+
* `report` counts; `reclaim` deletes.
|
|
35512
|
+
*
|
|
35513
|
+
* `.default('report')` is load-bearing: the settings form sends `{}` for a
|
|
35514
|
+
* button with no fields, and the safe reading of an unstated intent is the one
|
|
35515
|
+
* that destroys nothing.
|
|
35516
|
+
*/
|
|
35517
|
+
var MediaReclaimActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
35518
|
+
var MediaReclaimScopeSchema = require_dist._enum(["motion-still", "track-filmstrip"]);
|
|
35519
|
+
var MediaReclaimActionInputSchema = require_dist.object({
|
|
35520
|
+
mode: MediaReclaimActionModeSchema,
|
|
35521
|
+
/** Which populations to walk. Omitted = both. */
|
|
35522
|
+
scopes: require_dist.array(MediaReclaimScopeSchema).min(1).optional(),
|
|
35523
|
+
/** Restrict to these cameras. Omitted = every camera in retention scope. */
|
|
35524
|
+
deviceIds: require_dist.array(require_dist.number().int()).min(1).optional(),
|
|
35525
|
+
/** Start every device from the top instead of resuming its cursor. */
|
|
35526
|
+
restart: require_dist.boolean().optional(),
|
|
35527
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
35528
|
+
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
35529
|
+
/** Rows EXAMINED per camera in this pass — the pacing bound. */
|
|
35530
|
+
maxRowsPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
35531
|
+
/** Rows DELETED per camera in this pass — the blast-radius bound. */
|
|
35532
|
+
maxReclaimPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
35533
|
+
/** Bytes released across the whole run — the disk-contention bound. */
|
|
35534
|
+
maxBytesPerRun: require_dist.number().int().min(1).optional(),
|
|
35535
|
+
/** Wall-clock ceiling for the run. */
|
|
35536
|
+
budgetMinutes: require_dist.number().int().min(1).max(720).optional(),
|
|
35537
|
+
/**
|
|
35538
|
+
* Bytes per second the pass may release. The recordings volume sustains
|
|
35539
|
+
* ~4 MB/s and the archive needs it more than this does; the default is half
|
|
35540
|
+
* of that.
|
|
35541
|
+
*/
|
|
35542
|
+
throttleBytesPerSec: require_dist.number().int().min(64 * 1024).optional(),
|
|
35543
|
+
/**
|
|
35544
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
35545
|
+
* exclusively save rows, never select them.
|
|
35546
|
+
*/
|
|
35547
|
+
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
35548
|
+
});
|
|
35549
|
+
var MediaReclaimDeviceReportSchema = require_dist.object({
|
|
35550
|
+
deviceId: require_dist.number(),
|
|
35551
|
+
scope: MediaReclaimScopeSchema,
|
|
35552
|
+
examined: require_dist.number(),
|
|
35553
|
+
eligible: require_dist.number(),
|
|
35554
|
+
reclaimed: require_dist.number(),
|
|
35555
|
+
bytesReclaimed: require_dist.number(),
|
|
35556
|
+
/** Rows kept because the owning track is pinned (`staging`/`favourited`). */
|
|
35557
|
+
held: require_dist.number(),
|
|
35558
|
+
/** Rows kept because the owner could not be shown to be a motion event. */
|
|
35559
|
+
unresolved: require_dist.number(),
|
|
35560
|
+
withinGrace: require_dist.number(),
|
|
35561
|
+
/** Deletes that REFUSED — the blob survived or the volume could not answer.
|
|
35562
|
+
* The row was kept and the cursor did not pass it. */
|
|
35563
|
+
refused: require_dist.number(),
|
|
35564
|
+
stoppedBy: require_dist.string(),
|
|
35565
|
+
/** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
|
|
35566
|
+
nextCursor: require_dist.number()
|
|
35567
|
+
});
|
|
35568
|
+
var MediaReclaimActionResultSchema = require_dist.object({
|
|
35569
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
35570
|
+
ran: require_dist.boolean(),
|
|
35571
|
+
mode: require_dist._enum(["report", "reclaim"]),
|
|
35572
|
+
devices: require_dist.array(MediaReclaimDeviceReportSchema),
|
|
35573
|
+
totalExamined: require_dist.number(),
|
|
35574
|
+
totalEligible: require_dist.number(),
|
|
35575
|
+
totalReclaimed: require_dist.number(),
|
|
35576
|
+
totalBytesReclaimed: require_dist.number(),
|
|
35577
|
+
totalRefused: require_dist.number(),
|
|
35578
|
+
durationMs: require_dist.number(),
|
|
35579
|
+
/** Every device+scope reported `exhausted` — there is nothing left to do. */
|
|
35580
|
+
complete: require_dist.boolean()
|
|
35581
|
+
});
|
|
35582
|
+
var mediaReclaimActions = require_dist.defineCustomActions({ "retention.mediaReclaim": require_dist.customAction(MediaReclaimActionInputSchema, MediaReclaimActionResultSchema, { auth: "admin" }) });
|
|
35583
|
+
//#endregion
|
|
35338
35584
|
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
35339
35585
|
/**
|
|
35340
35586
|
* The operator button for the orphan audit.
|
|
@@ -36691,6 +36937,28 @@ function trackMediaScopesForKinds(trackId, kinds) {
|
|
|
36691
36937
|
return kinds.some((kind) => DETAIL_CROP_KINDS.includes(kind)) ? scopes : scopes.filter((scope) => scope.publishCropAs === null);
|
|
36692
36938
|
}
|
|
36693
36939
|
/**
|
|
36940
|
+
* The STORED kinds worth reading from one scope for a `kinds` request, or
|
|
36941
|
+
* `null` when the scope cannot contribute to it at all.
|
|
36942
|
+
*
|
|
36943
|
+
* This is the same vocabulary translation {@link collectTrackMedia} performs on
|
|
36944
|
+
* the way out, moved to the way IN so it can narrow the query instead of the
|
|
36945
|
+
* answer. A caller naming `faceCrop` must ask the face owner for its stored
|
|
36946
|
+
* `crop` — asking it for `faceCrop` matches nothing, and asking it for
|
|
36947
|
+
* everything opens blobs the answer will discard.
|
|
36948
|
+
*
|
|
36949
|
+
* `undefined` means "every kind": the honest translation of an unfiltered
|
|
36950
|
+
* request, and the only safe answer for the open-ended track scope.
|
|
36951
|
+
*
|
|
36952
|
+
* `null` (skip) is only ever returned for a DETAIL owner whose one publishable
|
|
36953
|
+
* kind was not asked for. The track scope is never skipped — its kinds are
|
|
36954
|
+
* open-ended, and dropping it on a guess is how a listing starts lying.
|
|
36955
|
+
*/
|
|
36956
|
+
function storedKindsForScope(scope, kinds) {
|
|
36957
|
+
if (kinds === void 0 || kinds.length === 0) return void 0;
|
|
36958
|
+
if (scope.publishCropAs === null) return kinds;
|
|
36959
|
+
return kinds.includes(scope.publishCropAs) ? [STORED_DETAIL_CROP_KIND] : null;
|
|
36960
|
+
}
|
|
36961
|
+
/**
|
|
36694
36962
|
* Fold every scope's rows into ONE track media listing: detail crops
|
|
36695
36963
|
* relabelled, everything filtered to `kinds`, ordered oldest first.
|
|
36696
36964
|
*
|
|
@@ -36827,8 +37095,10 @@ var AnalyticsQueryFacade = class {
|
|
|
36827
37095
|
return this.withMediaUrl(events);
|
|
36828
37096
|
}
|
|
36829
37097
|
async getEventMedia(input) {
|
|
36830
|
-
const
|
|
36831
|
-
|
|
37098
|
+
const store = this.deps.mediaStore();
|
|
37099
|
+
if (store === null) return [];
|
|
37100
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
37101
|
+
return (await store.listByOwner("event", input.eventId, input.deviceId, input.kind === void 0 ? void 0 : [input.kind])).map((row) => toWireMediaFile(row, prefix));
|
|
36832
37102
|
}
|
|
36833
37103
|
/**
|
|
36834
37104
|
* A track's media WITH the bytes, restricted to `kinds` when given.
|
|
@@ -36841,14 +37111,36 @@ var AnalyticsQueryFacade = class {
|
|
|
36841
37111
|
async getTrackMedia(input) {
|
|
36842
37112
|
const store = this.deps.mediaStore();
|
|
36843
37113
|
if (store === null) return [];
|
|
37114
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36844
37115
|
const scopes = trackMediaScopesForKinds(input.trackId, input.kinds);
|
|
36845
|
-
return collectTrackMedia(await Promise.all(scopes.map(async (scope) =>
|
|
36846
|
-
scope,
|
|
36847
|
-
|
|
36848
|
-
|
|
37116
|
+
return collectTrackMedia(await Promise.all(scopes.map(async (scope) => {
|
|
37117
|
+
const stored = storedKindsForScope(scope, input.kinds);
|
|
37118
|
+
if (stored === null) return {
|
|
37119
|
+
scope,
|
|
37120
|
+
rows: []
|
|
37121
|
+
};
|
|
37122
|
+
return {
|
|
37123
|
+
scope,
|
|
37124
|
+
rows: await store.listByOwner(scope.ownerKind, scope.ownerId, input.deviceId, stored)
|
|
37125
|
+
};
|
|
37126
|
+
})), (row, kind) => ({
|
|
36849
37127
|
...row,
|
|
36850
37128
|
kind
|
|
36851
|
-
}), input.kinds);
|
|
37129
|
+
}), input.kinds).map((row) => toWireMediaFile(row, prefix));
|
|
37130
|
+
}
|
|
37131
|
+
/**
|
|
37132
|
+
* An event's media WITHOUT the bytes.
|
|
37133
|
+
*
|
|
37134
|
+
* Index rows only — it never opens a blob. The track detail modal unions
|
|
37135
|
+
* this with {@link listTrackMedia} to build its filmstrip and then fetches
|
|
37136
|
+
* each tile from the `event-media` plane by key, so the bytes this used to
|
|
37137
|
+
* carry were read, base64'd, shipped and discarded.
|
|
37138
|
+
*/
|
|
37139
|
+
async listEventMedia(input) {
|
|
37140
|
+
const store = this.deps.mediaStore();
|
|
37141
|
+
if (store === null) return [];
|
|
37142
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
37143
|
+
return (await store.listInfoByOwner("event", input.eventId, input.deviceId)).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36852
37144
|
}
|
|
36853
37145
|
/**
|
|
36854
37146
|
* A track's media WITHOUT the bytes.
|
|
@@ -36866,13 +37158,14 @@ var AnalyticsQueryFacade = class {
|
|
|
36866
37158
|
async listTrackMedia(input) {
|
|
36867
37159
|
const store = this.deps.mediaStore();
|
|
36868
37160
|
if (store === null) return [];
|
|
37161
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36869
37162
|
return collectTrackMedia(await Promise.all(trackMediaOwnerScopes(input.trackId).map(async (scope) => ({
|
|
36870
37163
|
scope,
|
|
36871
37164
|
rows: await store.listInfoByOwner(scope.ownerKind, scope.ownerId, input.deviceId)
|
|
36872
37165
|
}))), (row, kind) => ({
|
|
36873
37166
|
...row,
|
|
36874
37167
|
kind
|
|
36875
|
-
}), void 0);
|
|
37168
|
+
}), void 0).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36876
37169
|
}
|
|
36877
37170
|
async pruneEventsBefore(input) {
|
|
36878
37171
|
const eventStore = this.deps.eventStore();
|
|
@@ -36899,10 +37192,34 @@ var AnalyticsQueryFacade = class {
|
|
|
36899
37192
|
}
|
|
36900
37193
|
/**
|
|
36901
37194
|
* Durable event-store footprint for the events-management UI: event ROWS
|
|
36902
|
-
* (motion + object + audio)
|
|
36903
|
-
*
|
|
36904
|
-
*
|
|
36905
|
-
*
|
|
37195
|
+
* (motion + object + audio) per camera + total, and event-owned media BYTES
|
|
37196
|
+
* on disk per camera + total. Cameras with neither rows nor bytes are
|
|
37197
|
+
* omitted; the rest come back ordered by bytes, descending, which is the
|
|
37198
|
+
* order the operator's table renders.
|
|
37199
|
+
*
|
|
37200
|
+
* ## The device set is a union of THREE sources
|
|
37201
|
+
*
|
|
37202
|
+
* It used to be `media ∪ tracks`, and a camera holding events but no media
|
|
37203
|
+
* row and no persisted track was therefore invisible — not listed, and its
|
|
37204
|
+
* events missing from `totalRows`. That combination is not exotic: media
|
|
37205
|
+
* retention, a media deletion and `pruneTracksBefore` all run on clocks of
|
|
37206
|
+
* their own, and every one of them can outlive the event rows. The number
|
|
37207
|
+
* this method exists to produce is the denominator for a deletion, so an
|
|
37208
|
+
* undercount is the one failure mode that matters, and it was silent.
|
|
37209
|
+
*
|
|
37210
|
+
* Events now contribute their own device set, so a camera is present if ANY
|
|
37211
|
+
* of the three sources has ever heard of it.
|
|
37212
|
+
*
|
|
37213
|
+
* ## The cost is the answer, not the table
|
|
37214
|
+
*
|
|
37215
|
+
* Three grouped reads for the event rows (one per event collection, never
|
|
37216
|
+
* three per camera), one grouped read plus one aggregate per camera for the
|
|
37217
|
+
* media bytes, one for the track device set. The retired shape paid ~1 250
|
|
37218
|
+
* round trips for the media half alone and timed out at 60 s on every call.
|
|
37219
|
+
* See {@link MediaStore.footprintByDevice} and {@link EventStore.countByDevice}.
|
|
37220
|
+
*
|
|
37221
|
+
* A store that cannot answer THROWS rather than returning zeroes — see the
|
|
37222
|
+
* same note on both.
|
|
36906
37223
|
*/
|
|
36907
37224
|
async getEventStoreFootprint() {
|
|
36908
37225
|
const eventStore = this.deps.eventStore();
|
|
@@ -36912,15 +37229,16 @@ var AnalyticsQueryFacade = class {
|
|
|
36912
37229
|
totalBytes: 0,
|
|
36913
37230
|
devices: []
|
|
36914
37231
|
};
|
|
36915
|
-
const mediaByDevice = await mediaStore.footprintByDevice();
|
|
37232
|
+
const [mediaByDevice, eventRowsByDevice] = await Promise.all([mediaStore.footprintByDevice(), eventStore.countByDevice()]);
|
|
36916
37233
|
const deviceIds = new Set(mediaByDevice.keys());
|
|
37234
|
+
for (const id of eventRowsByDevice.keys()) deviceIds.add(id);
|
|
36917
37235
|
const trackStore = this.deps.trackStore();
|
|
36918
37236
|
if (trackStore) for (const id of await trackStore.listDeviceIds()) deviceIds.add(id);
|
|
36919
37237
|
const devices = [];
|
|
36920
37238
|
let totalRows = 0;
|
|
36921
37239
|
let totalBytes = 0;
|
|
36922
37240
|
for (const deviceId of deviceIds) {
|
|
36923
|
-
const rows =
|
|
37241
|
+
const rows = eventRowsByDevice.get(deviceId) ?? 0;
|
|
36924
37242
|
const bytes = mediaByDevice.get(deviceId)?.bytes ?? 0;
|
|
36925
37243
|
if (rows === 0 && bytes === 0) continue;
|
|
36926
37244
|
totalRows += rows;
|
|
@@ -36939,6 +37257,26 @@ var AnalyticsQueryFacade = class {
|
|
|
36939
37257
|
};
|
|
36940
37258
|
}
|
|
36941
37259
|
/**
|
|
37260
|
+
* The media footprint by KIND — the axis that says what is safe to delete.
|
|
37261
|
+
* See {@link MediaStore.footprintByKind}.
|
|
37262
|
+
*
|
|
37263
|
+
* An absent media store is an empty breakdown, not a refusal: it is the same
|
|
37264
|
+
* "the addon has not finished booting" branch every other method here takes,
|
|
37265
|
+
* and the zeroes are accompanied by an empty `kinds` list, which no reader
|
|
37266
|
+
* can mistake for a measured "there is nothing".
|
|
37267
|
+
*/
|
|
37268
|
+
async getEventMediaFootprintByKind(input) {
|
|
37269
|
+
const mediaStore = this.deps.mediaStore();
|
|
37270
|
+
if (!mediaStore) return {
|
|
37271
|
+
totalRows: 0,
|
|
37272
|
+
totalBytes: 0,
|
|
37273
|
+
kinds: [],
|
|
37274
|
+
unaccountedRows: 0,
|
|
37275
|
+
unaccountedBytes: 0
|
|
37276
|
+
};
|
|
37277
|
+
return mediaStore.footprintByKind(input.deviceId);
|
|
37278
|
+
}
|
|
37279
|
+
/**
|
|
36942
37280
|
* Cluster-wide prune of events with `timestamp < olderThanMs` across every
|
|
36943
37281
|
* camera (via the device-agnostic `evictBefore`), deleting each pruned event's
|
|
36944
37282
|
* media in lockstep. Logs one global (`deviceId:null`) events ops-log row.
|
|
@@ -39317,6 +39655,31 @@ function createEventMediaHandler(deps) {
|
|
|
39317
39655
|
}
|
|
39318
39656
|
//#endregion
|
|
39319
39657
|
//#region src/pipeline-analytics/event-thumbnail-resolver.ts
|
|
39658
|
+
/**
|
|
39659
|
+
* Rank `candidates` with `pick`, then open ONE blob — the winner's.
|
|
39660
|
+
*
|
|
39661
|
+
* The ladders decide on `kind` alone, so the bytes are dead weight until the
|
|
39662
|
+
* choice is made. Handing them pre-read rows meant a tile that serves the
|
|
39663
|
+
* 31 KB `thumbnailSmall` first read the whole 3 597 219-byte set, including a
|
|
39664
|
+
* 2.8 MB `keyFrame` no list view has ever rendered.
|
|
39665
|
+
*
|
|
39666
|
+
* A winner whose blob is GONE is dropped and the ladder re-run on the rest.
|
|
39667
|
+
* That fall-through is not an extra: `listByOwner` used to provide it for free
|
|
39668
|
+
* by skipping unreadable rows before the ladder ever saw them, and without it
|
|
39669
|
+
* an index row that outlived its file would 404 a tile that used to render.
|
|
39670
|
+
* Bounded by the candidate count — each attempt removes one row.
|
|
39671
|
+
*/
|
|
39672
|
+
async function readWinningBlob(candidates, pick, readBlob) {
|
|
39673
|
+
let remaining = candidates;
|
|
39674
|
+
while (remaining.length > 0) {
|
|
39675
|
+
const winner = pick(remaining);
|
|
39676
|
+
if (winner === void 0) return null;
|
|
39677
|
+
const bytes = await readBlob(winner.key);
|
|
39678
|
+
if (bytes !== null) return bytes;
|
|
39679
|
+
remaining = remaining.filter((row) => row.key !== winner.key);
|
|
39680
|
+
}
|
|
39681
|
+
return null;
|
|
39682
|
+
}
|
|
39320
39683
|
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
39321
39684
|
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
39322
39685
|
* for new tracks (served only for old stored rows). Undefined when the event
|
|
@@ -39367,15 +39730,24 @@ async function collectCropForcedMedia(source, id) {
|
|
|
39367
39730
|
};
|
|
39368
39731
|
}
|
|
39369
39732
|
/**
|
|
39370
|
-
*
|
|
39371
|
-
* has any, else the owning track's
|
|
39372
|
-
*
|
|
39733
|
+
* Which rows the default path should serve from, and how to rank them: the
|
|
39734
|
+
* event's OWN media when it has any, else the owning track's.
|
|
39735
|
+
*
|
|
39736
|
+
* It yields the candidate SET rather than a winner so the caller can open one
|
|
39737
|
+
* blob and, if that blob has gone, re-rank the rest ({@link readWinningBlob}).
|
|
39738
|
+
* Returning a single row would force the caller to read it blind and 404 a tile
|
|
39739
|
+
* whose index row outlived its file.
|
|
39373
39740
|
*/
|
|
39374
|
-
async function
|
|
39375
|
-
|
|
39376
|
-
|
|
39741
|
+
async function collectDefaultEventMedia(deps) {
|
|
39742
|
+
if (deps.eventFiles.length > 0) return {
|
|
39743
|
+
candidates: deps.eventFiles,
|
|
39744
|
+
pick: pickEventOwnedMedia
|
|
39745
|
+
};
|
|
39377
39746
|
const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
|
|
39378
|
-
return
|
|
39747
|
+
return {
|
|
39748
|
+
candidates: await deps.listTrackMedia(trackId),
|
|
39749
|
+
pick: pickTrackFallbackMedia
|
|
39750
|
+
};
|
|
39379
39751
|
}
|
|
39380
39752
|
//#endregion
|
|
39381
39753
|
//#region src/pipeline-analytics/face-settings.ts
|
|
@@ -44573,6 +44945,15 @@ function snapshot(j) {
|
|
|
44573
44945
|
*/
|
|
44574
44946
|
var LOCATION_ID_FIELD = "locationId";
|
|
44575
44947
|
/**
|
|
44948
|
+
* Unstamped rows claimed per bulk seal statement.
|
|
44949
|
+
*
|
|
44950
|
+
* Bounds two things at once: the number of bound parameters in the `IN` list,
|
|
44951
|
+
* and how long one `UPDATE` holds the write lock on the database every process
|
|
44952
|
+
* in the cluster reads configuration through. Deliberately well under SQLite's
|
|
44953
|
+
* variable ceiling, and small enough that a cancel lands within one statement.
|
|
44954
|
+
*/
|
|
44955
|
+
var SEAL_BATCH_SIZE = 500;
|
|
44956
|
+
/**
|
|
44576
44957
|
* True when the row names NO location — the population `seal` exists for.
|
|
44577
44958
|
*
|
|
44578
44959
|
* Deliberately NOT `mediaRowLocation(data) === DEFAULT_MEDIA_LOCATION`: that
|
|
@@ -44804,6 +45185,17 @@ var MediaRelocateEngine = class {
|
|
|
44804
45185
|
}
|
|
44805
45186
|
async countSelected(mode, toLocationId, stop) {
|
|
44806
45187
|
try {
|
|
45188
|
+
if (mode === "seal") {
|
|
45189
|
+
let sealable = 0;
|
|
45190
|
+
for (const collection of collectionsFor(mode)) {
|
|
45191
|
+
if (stop()) return null;
|
|
45192
|
+
sealable += await this.deps.store.count.query({
|
|
45193
|
+
collection: collection.name,
|
|
45194
|
+
filter: { where: { [LOCATION_ID_FIELD]: null } }
|
|
45195
|
+
});
|
|
45196
|
+
}
|
|
45197
|
+
return { rows: sealable };
|
|
45198
|
+
}
|
|
44807
45199
|
let rows = 0;
|
|
44808
45200
|
for (const collection of collectionsFor(mode)) {
|
|
44809
45201
|
const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
|
|
@@ -44835,6 +45227,14 @@ var MediaRelocateEngine = class {
|
|
|
44835
45227
|
* gets believed. */
|
|
44836
45228
|
let pathlessRows = 0;
|
|
44837
45229
|
job.filesTotal = (await this.countSelected(job.mode, job.toLocationId, () => job.cancelRequested))?.rows ?? null;
|
|
45230
|
+
if (job.mode === "seal") {
|
|
45231
|
+
for (const collection of collectionsFor(job.mode)) {
|
|
45232
|
+
if (job.cancelRequested) break;
|
|
45233
|
+
await this.sealCollection(collection.name, job);
|
|
45234
|
+
}
|
|
45235
|
+
job.state = job.cancelRequested ? "cancelled" : "done";
|
|
45236
|
+
return;
|
|
45237
|
+
}
|
|
44838
45238
|
for (const collection of collectionsFor(job.mode)) {
|
|
44839
45239
|
if (job.cancelRequested) break;
|
|
44840
45240
|
const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
|
|
@@ -44842,20 +45242,6 @@ var MediaRelocateEngine = class {
|
|
|
44842
45242
|
if (job.cancelRequested) break;
|
|
44843
45243
|
const data = row.data;
|
|
44844
45244
|
if (!selectsRow(job.mode, job.toLocationId, data, isRetrain)) continue;
|
|
44845
|
-
if (job.mode === "seal") {
|
|
44846
|
-
try {
|
|
44847
|
-
await this.stamp(collection.name, row.id, data, job.toLocationId);
|
|
44848
|
-
job.filesMoved++;
|
|
44849
|
-
} catch (err) {
|
|
44850
|
-
failedRows++;
|
|
44851
|
-
this.deps.logger.debug("event-media seal row failed", { meta: {
|
|
44852
|
-
collection: collection.name,
|
|
44853
|
-
key: row.id,
|
|
44854
|
-
error: String(err)
|
|
44855
|
-
} });
|
|
44856
|
-
}
|
|
44857
|
-
continue;
|
|
44858
|
-
}
|
|
44859
45245
|
const from = mediaRowLocation(data);
|
|
44860
45246
|
const relativePath = String(data["path"] ?? "");
|
|
44861
45247
|
if (relativePath.length === 0) {
|
|
@@ -44918,6 +45304,73 @@ var MediaRelocateEngine = class {
|
|
|
44918
45304
|
this.deps.onFinished?.(snapshot(job));
|
|
44919
45305
|
}
|
|
44920
45306
|
}
|
|
45307
|
+
/**
|
|
45308
|
+
* Stamp every unstamped row of one collection with the job's target, in
|
|
45309
|
+
* bounded bulk statements.
|
|
45310
|
+
*
|
|
45311
|
+
* ## What this replaced
|
|
45312
|
+
*
|
|
45313
|
+
* A per-row loop: page the whole collection through {@link pageRows}, then
|
|
45314
|
+
* one `store.set.mutate` per selected row — a full-row UPSERT rewriting every
|
|
45315
|
+
* column to change one field, one RPC each, plus a second full walk before it
|
|
45316
|
+
* to produce the denominator. Measured on the live hub against the 464 902
|
|
45317
|
+
* unstamped rows: **2.3 rows/s, falling to 0.7 rows/s under a competing
|
|
45318
|
+
* drain**, i.e. over two and a half days for a metadata stamp that reads no
|
|
45319
|
+
* file and moves no byte. It was cancelled. This is the write-side twin of
|
|
45320
|
+
* the read-side walk {@link countUnstamped} retired, and the fourth instance
|
|
45321
|
+
* of the same pattern found in one day — work looped over the door instead of
|
|
45322
|
+
* pushed into the statement.
|
|
45323
|
+
*
|
|
45324
|
+
* ## The shape
|
|
45325
|
+
*
|
|
45326
|
+
* Probe the indexed `IS NULL` range for a batch of ids, then ONE
|
|
45327
|
+
* `updateWhere` over those ids. Two round trips per {@link SEAL_BATCH_SIZE}
|
|
45328
|
+
* rows instead of one write per row.
|
|
45329
|
+
*
|
|
45330
|
+
* The probe carries no cursor and needs none: the predicate CONSUMES itself.
|
|
45331
|
+
* A stamped row leaves the range, so the next probe returns the next batch,
|
|
45332
|
+
* and a run that is interrupted anywhere simply resumes on what is left. That
|
|
45333
|
+
* is also why the seal is idempotent (a second run probes an empty range and
|
|
45334
|
+
* stamps nothing) and why it cannot repoint a row that already names a
|
|
45335
|
+
* different location — `IS NULL` never selects one.
|
|
45336
|
+
*
|
|
45337
|
+
* A single unbounded `UPDATE … WHERE locationId IS NULL` would be one
|
|
45338
|
+
* statement instead of ~1 860, and is not used: half a million row rewrites,
|
|
45339
|
+
* each maintaining four indexes, is an unbounded statement holding the write
|
|
45340
|
+
* lock on a database the whole cluster reads configuration through, under an
|
|
45341
|
+
* RPC deadline that must not be raised. Batching bounds the statement, keeps
|
|
45342
|
+
* `cancel` responsive between batches, and keeps `filesMoved` honest.
|
|
45343
|
+
*
|
|
45344
|
+
* ## The termination guard
|
|
45345
|
+
*
|
|
45346
|
+
* If the probe finds rows and the update reports **0**, this THROWS. That is
|
|
45347
|
+
* not defensive noise: it is precisely what a backend compiling
|
|
45348
|
+
* `locationId = NULL` does — matches nothing, reports success (D312) — and
|
|
45349
|
+
* against it a self-consuming loop never terminates and a non-looping one
|
|
45350
|
+
* reports a completed seal having stamped nothing. Zero unstamped rows is the
|
|
45351
|
+
* single value the migration gate reads as "safe to cut over".
|
|
45352
|
+
*/
|
|
45353
|
+
async sealCollection(collection, job) {
|
|
45354
|
+
for (;;) {
|
|
45355
|
+
if (job.cancelRequested) return;
|
|
45356
|
+
const batch = await this.deps.store.query.query({
|
|
45357
|
+
collection,
|
|
45358
|
+
filter: {
|
|
45359
|
+
where: { [LOCATION_ID_FIELD]: null },
|
|
45360
|
+
limit: SEAL_BATCH_SIZE
|
|
45361
|
+
},
|
|
45362
|
+
columns: [LOCATION_ID_FIELD]
|
|
45363
|
+
});
|
|
45364
|
+
if (batch.length === 0) return;
|
|
45365
|
+
const { updated } = await this.deps.store.updateWhere.mutate({
|
|
45366
|
+
collection,
|
|
45367
|
+
filter: { whereIn: { id: batch.map((row) => row.id) } },
|
|
45368
|
+
data: { [LOCATION_ID_FIELD]: job.toLocationId }
|
|
45369
|
+
});
|
|
45370
|
+
if (updated === 0) throw new Error(`event-media seal stamped 0 of ${batch.length} unstamped row(s) in "${collection}" — the bulk predicate matched nothing, so the population cannot be sealed`);
|
|
45371
|
+
job.filesMoved += updated;
|
|
45372
|
+
}
|
|
45373
|
+
}
|
|
44921
45374
|
async stamp(collection, key, data, locationId) {
|
|
44922
45375
|
await this.deps.store.set.mutate({
|
|
44923
45376
|
collection,
|
|
@@ -47483,6 +47936,70 @@ var PackageDropDetector = class {
|
|
|
47483
47936
|
}
|
|
47484
47937
|
};
|
|
47485
47938
|
//#endregion
|
|
47939
|
+
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
47940
|
+
/**
|
|
47941
|
+
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
47942
|
+
*
|
|
47943
|
+
* 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
|
|
47944
|
+
* routinely pokes a little outside the vehicle box, and requiring total
|
|
47945
|
+
* containment would count almost none of them. 0.8 and not 0.5: half a person
|
|
47946
|
+
* box overlapping a car is what a pedestrian walking in front of one looks
|
|
47947
|
+
* like, and counting those would make the number mean "somebody was near a
|
|
47948
|
+
* car", which is not a defect.
|
|
47949
|
+
*/
|
|
47950
|
+
var CONTAINMENT_THRESHOLD = .8;
|
|
47951
|
+
/**
|
|
47952
|
+
* The classes that count as a vehicle for this test.
|
|
47953
|
+
*
|
|
47954
|
+
* A local list rather than a shared taxonomy on purpose: it exists to describe
|
|
47955
|
+
* ONE measurement, and a shared enum would make a change here silently change
|
|
47956
|
+
* detection behaviour somewhere else.
|
|
47957
|
+
*/
|
|
47958
|
+
var VEHICLE_CLASSES$1 = [
|
|
47959
|
+
"car",
|
|
47960
|
+
"truck",
|
|
47961
|
+
"bus",
|
|
47962
|
+
"motorcycle",
|
|
47963
|
+
"vehicle"
|
|
47964
|
+
];
|
|
47965
|
+
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
47966
|
+
function intersectionArea(a, b) {
|
|
47967
|
+
const left = Math.max(a.x, b.x);
|
|
47968
|
+
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
47969
|
+
const top = Math.max(a.y, b.y);
|
|
47970
|
+
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
47971
|
+
if (right <= left || bottom <= top) return 0;
|
|
47972
|
+
return (right - left) * (bottom - top);
|
|
47973
|
+
}
|
|
47974
|
+
/**
|
|
47975
|
+
* Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
|
|
47976
|
+
*
|
|
47977
|
+
* `0` for a degenerate person box: a zero-area box is inside everything and
|
|
47978
|
+
* inside nothing, and answering `1` would count every one of them.
|
|
47979
|
+
*/
|
|
47980
|
+
function containedFraction(person, vehicle) {
|
|
47981
|
+
const area = person.w * person.h;
|
|
47982
|
+
if (area <= 0) return 0;
|
|
47983
|
+
return intersectionArea(person, vehicle) / area;
|
|
47984
|
+
}
|
|
47985
|
+
/**
|
|
47986
|
+
* Is this person box contained in any vehicle box in the same frame?
|
|
47987
|
+
*
|
|
47988
|
+
* Returns the fraction for the BEST-containing vehicle when it clears
|
|
47989
|
+
* {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
|
|
47990
|
+
* a counter that only ever says "yes" cannot later be re-thresholded against
|
|
47991
|
+
* what was actually measured.
|
|
47992
|
+
*/
|
|
47993
|
+
function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
|
|
47994
|
+
let best = 0;
|
|
47995
|
+
for (const neighbour of neighbours) {
|
|
47996
|
+
if (!VEHICLE_CLASSES$1.includes(neighbour.className)) continue;
|
|
47997
|
+
const fraction = containedFraction(person, neighbour.bbox);
|
|
47998
|
+
if (fraction > best) best = fraction;
|
|
47999
|
+
}
|
|
48000
|
+
return best >= threshold ? best : null;
|
|
48001
|
+
}
|
|
48002
|
+
//#endregion
|
|
47486
48003
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
|
|
47487
48004
|
/**
|
|
47488
48005
|
* stationary-evidence — how a parked object EARNS a registry entry.
|
|
@@ -50014,83 +50531,6 @@ function buildRecordedStillPorts(input) {
|
|
|
50014
50531
|
})
|
|
50015
50532
|
};
|
|
50016
50533
|
}
|
|
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
50534
|
//#endregion
|
|
50095
50535
|
//#region src/pipeline-analytics/retention/embedding-orphan-scan.ts
|
|
50096
50536
|
/** The scope id this leg reports and persists its cursor under. */
|
|
@@ -50207,6 +50647,83 @@ async function scanEmbeddingOrphans(deps, limits, cursor) {
|
|
|
50207
50647
|
nextCursor: Math.max(0, offset)
|
|
50208
50648
|
};
|
|
50209
50649
|
}
|
|
50650
|
+
function errorMessage(err) {
|
|
50651
|
+
return err instanceof Error ? err.message : String(err);
|
|
50652
|
+
}
|
|
50653
|
+
async function reconcileGallerySamples(deps, options) {
|
|
50654
|
+
let checked = 0;
|
|
50655
|
+
let reclaimed = 0;
|
|
50656
|
+
let keyless = 0;
|
|
50657
|
+
let skippedGrace = 0;
|
|
50658
|
+
let stoppedByCeiling = false;
|
|
50659
|
+
const identities = await deps.listIdentities();
|
|
50660
|
+
for (const identity of identities) {
|
|
50661
|
+
const samples = await deps.listSamples(identity.id);
|
|
50662
|
+
for (const sample of samples) {
|
|
50663
|
+
if (sample.mediaKey === void 0) {
|
|
50664
|
+
keyless += 1;
|
|
50665
|
+
continue;
|
|
50666
|
+
}
|
|
50667
|
+
if (options.now - sample.addedAt < options.graceMs) {
|
|
50668
|
+
skippedGrace += 1;
|
|
50669
|
+
continue;
|
|
50670
|
+
}
|
|
50671
|
+
if (reclaimed >= options.maxReclaimPerRun) {
|
|
50672
|
+
stoppedByCeiling = true;
|
|
50673
|
+
deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
|
|
50674
|
+
return {
|
|
50675
|
+
checked,
|
|
50676
|
+
reclaimed,
|
|
50677
|
+
keyless,
|
|
50678
|
+
skippedGrace,
|
|
50679
|
+
stoppedByCeiling
|
|
50680
|
+
};
|
|
50681
|
+
}
|
|
50682
|
+
let state;
|
|
50683
|
+
try {
|
|
50684
|
+
state = await deps.blobState(sample.mediaKey);
|
|
50685
|
+
} catch (err) {
|
|
50686
|
+
const reason = errorMessage(err);
|
|
50687
|
+
deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
|
|
50688
|
+
sampleId: sample.id,
|
|
50689
|
+
identity: identity.name,
|
|
50690
|
+
error: reason
|
|
50691
|
+
} });
|
|
50692
|
+
return {
|
|
50693
|
+
checked,
|
|
50694
|
+
reclaimed,
|
|
50695
|
+
keyless,
|
|
50696
|
+
skippedGrace,
|
|
50697
|
+
stoppedByCeiling,
|
|
50698
|
+
aborted: reason
|
|
50699
|
+
};
|
|
50700
|
+
}
|
|
50701
|
+
checked += 1;
|
|
50702
|
+
if (state === "exists") continue;
|
|
50703
|
+
await deps.removeSample(identity.id, sample.id);
|
|
50704
|
+
reclaimed += 1;
|
|
50705
|
+
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
50706
|
+
sampleId: sample.id,
|
|
50707
|
+
identity: identity.name,
|
|
50708
|
+
mediaKey: sample.mediaKey,
|
|
50709
|
+
state
|
|
50710
|
+
} });
|
|
50711
|
+
}
|
|
50712
|
+
}
|
|
50713
|
+
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
50714
|
+
checked,
|
|
50715
|
+
reclaimed,
|
|
50716
|
+
keyless,
|
|
50717
|
+
skippedGrace
|
|
50718
|
+
} });
|
|
50719
|
+
return {
|
|
50720
|
+
checked,
|
|
50721
|
+
reclaimed,
|
|
50722
|
+
keyless,
|
|
50723
|
+
skippedGrace,
|
|
50724
|
+
stoppedByCeiling
|
|
50725
|
+
};
|
|
50726
|
+
}
|
|
50210
50727
|
/**
|
|
50211
50728
|
* Where the completion marker lives.
|
|
50212
50729
|
*
|
|
@@ -51585,6 +52102,43 @@ var EventStore = class {
|
|
|
51585
52102
|
return motion + object + audio;
|
|
51586
52103
|
}
|
|
51587
52104
|
/**
|
|
52105
|
+
* Persisted event rows (motion + object + audio) for EVERY device that has
|
|
52106
|
+
* any — three round trips for the whole fleet.
|
|
52107
|
+
*
|
|
52108
|
+
* {@link countForDevice} answers one camera in three `COUNT(*)`s, which is
|
|
52109
|
+
* right for one camera and wrong in a loop: the events-management footprint
|
|
52110
|
+
* called it once per device, so the cost of "how many events does each camera
|
|
52111
|
+
* hold" grew with the camera count for a result that is a few dozen integers.
|
|
52112
|
+
*
|
|
52113
|
+
* A histogram on `deviceId` with `bucketSize: 1` is `GROUP BY deviceId`, and
|
|
52114
|
+
* every event collection already carries `idx_<kind>_device_ts`, which leads
|
|
52115
|
+
* with `deviceId` — so the planner serves each of these from a covering index
|
|
52116
|
+
* with no table access at all. `bucket` IS the deviceId (the column is
|
|
52117
|
+
* INTEGER, the bucket expression is `CAST((deviceId - 0) / 1 AS INTEGER)`).
|
|
52118
|
+
*
|
|
52119
|
+
* Not best-effort: a failure propagates. The one caller is sizing a deletion,
|
|
52120
|
+
* and a camera silently reported as holding zero events is the answer that
|
|
52121
|
+
* must never come from a read that did not happen.
|
|
52122
|
+
*/
|
|
52123
|
+
async countByDevice() {
|
|
52124
|
+
const perCollection = await Promise.all([
|
|
52125
|
+
MOTION_EVENTS_COLLECTION,
|
|
52126
|
+
OBJECT_EVENTS_COLLECTION,
|
|
52127
|
+
AUDIO_EVENTS_COLLECTION
|
|
52128
|
+
].map((collection) => this.store.histogram.query({
|
|
52129
|
+
collection,
|
|
52130
|
+
field: "deviceId",
|
|
52131
|
+
bucketSize: 1,
|
|
52132
|
+
origin: 0
|
|
52133
|
+
})));
|
|
52134
|
+
const out = /* @__PURE__ */ new Map();
|
|
52135
|
+
for (const buckets of perCollection) for (const bucket of buckets) {
|
|
52136
|
+
if (bucket.count === 0 || !Number.isFinite(bucket.bucket)) continue;
|
|
52137
|
+
out.set(bucket.bucket, (out.get(bucket.bucket) ?? 0) + bucket.count);
|
|
52138
|
+
}
|
|
52139
|
+
return out;
|
|
52140
|
+
}
|
|
52141
|
+
/**
|
|
51588
52142
|
* Return per-kind event counts in equal-width time buckets.
|
|
51589
52143
|
*
|
|
51590
52144
|
* Each bucket maps to a `bucketStart = since + i * bucketMs`.
|
|
@@ -53269,14 +53823,14 @@ function isAfterCursor(row, cursor) {
|
|
|
53269
53823
|
if (row.lastSeen < cursor.lastSeen) return true;
|
|
53270
53824
|
return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
|
|
53271
53825
|
}
|
|
53272
|
-
var VEHICLE_CLASSES
|
|
53826
|
+
var VEHICLE_CLASSES = new Set([
|
|
53273
53827
|
"vehicle",
|
|
53274
53828
|
"car",
|
|
53275
53829
|
"truck",
|
|
53276
53830
|
"bus"
|
|
53277
53831
|
]);
|
|
53278
53832
|
function motionlessMaxPxForClass(className) {
|
|
53279
|
-
return VEHICLE_CLASSES
|
|
53833
|
+
return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
|
|
53280
53834
|
}
|
|
53281
53835
|
function netDisplacementPx(positions) {
|
|
53282
53836
|
const first = positions[0];
|
|
@@ -57036,70 +57590,6 @@ function hasWrappedDetection(entries) {
|
|
|
57036
57590
|
return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
|
|
57037
57591
|
}
|
|
57038
57592
|
//#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
57593
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
57104
57594
|
function createCaptureCrop(deps) {
|
|
57105
57595
|
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
@@ -63541,6 +64031,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63541
64031
|
* Set once the data-plane is registered; null until then (e.g. no
|
|
63542
64032
|
* dataPlane facility in the current environment). */
|
|
63543
64033
|
eventMediaBaseUrl = null;
|
|
64034
|
+
/**
|
|
64035
|
+
* The SAME path, derived rather than observed — a pure function of the addon
|
|
64036
|
+
* id, so it exists before `serve()` is attempted and survives its failure.
|
|
64037
|
+
*
|
|
64038
|
+
* `eventMediaBaseUrl` above answers "is the plane up?", which is the right
|
|
64039
|
+
* question for the OPTIONAL `mediaUrl` stamp on a slim event projection.
|
|
64040
|
+
* It is the wrong question for a media row's `url`, which is that row's only
|
|
64041
|
+
* address: a row that dropped its address on a boot race would be
|
|
64042
|
+
* indistinguishable from a row with no bytes, and this repo has already paid
|
|
64043
|
+
* for one read that quietly returned less than it was asked for.
|
|
64044
|
+
*/
|
|
64045
|
+
eventMediaPathPrefix() {
|
|
64046
|
+
return `/addon/${this.ctx.id}/event-media`;
|
|
64047
|
+
}
|
|
63544
64048
|
lastActiveTrackIds = /* @__PURE__ */ new Map();
|
|
63545
64049
|
/** See `pipeline/suppressed-births.ts` — rejected births must not be
|
|
63546
64050
|
* re-upserted, and must be forgotten when the tracker drops the id. */
|
|
@@ -63791,6 +64295,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63791
64295
|
sensorEventStore: () => this.sensorEventStore,
|
|
63792
64296
|
opsLog: () => this.eventsOpsLog,
|
|
63793
64297
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
64298
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
63794
64299
|
logger: () => this.ctx.logger,
|
|
63795
64300
|
zoneCatalogue: () => this.zoneCatalogue
|
|
63796
64301
|
});
|
|
@@ -66333,6 +66838,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66333
66838
|
logger: this.ctx.logger,
|
|
66334
66839
|
refreshGallery: () => this.faceRecognizer?.refreshGallery(),
|
|
66335
66840
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66841
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66336
66842
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload)
|
|
66337
66843
|
});
|
|
66338
66844
|
this.faceGalleryProvider = faceGallery;
|
|
@@ -66347,6 +66853,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66347
66853
|
this.plateRecognizer?.refreshGallery();
|
|
66348
66854
|
},
|
|
66349
66855
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66856
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66350
66857
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
|
|
66351
66858
|
});
|
|
66352
66859
|
return [
|
|
@@ -70032,6 +70539,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70032
70539
|
async listTrackMedia(input) {
|
|
70033
70540
|
return this.queryFacade.listTrackMedia(input);
|
|
70034
70541
|
}
|
|
70542
|
+
/** What media an EVENT has, without any of it — the same manifest, for the
|
|
70543
|
+
* half of the detail modal's filmstrip that is event-owned. */
|
|
70544
|
+
async listEventMedia(input) {
|
|
70545
|
+
return this.queryFacade.listEventMedia(input);
|
|
70546
|
+
}
|
|
70035
70547
|
/**
|
|
70036
70548
|
* Search object events by text using CLIP cosine similarity.
|
|
70037
70549
|
*
|
|
@@ -71350,6 +71862,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71350
71862
|
async getEventStoreFootprint() {
|
|
71351
71863
|
return this.queryFacade.getEventStoreFootprint();
|
|
71352
71864
|
}
|
|
71865
|
+
async getEventMediaFootprintByKind(input) {
|
|
71866
|
+
return this.queryFacade.getEventMediaFootprintByKind(input);
|
|
71867
|
+
}
|
|
71353
71868
|
async pruneEvents(input) {
|
|
71354
71869
|
return this.queryFacade.pruneEvents(input);
|
|
71355
71870
|
}
|
|
@@ -71752,31 +72267,44 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71752
72267
|
return media;
|
|
71753
72268
|
}
|
|
71754
72269
|
}
|
|
72270
|
+
/**
|
|
72271
|
+
* Resolve an event/track id to ONE tile's bytes.
|
|
72272
|
+
*
|
|
72273
|
+
* Every ladder below ranks rows by `kind` alone, so this walks the INDEX
|
|
72274
|
+
* (`listInfoByOwner`, no blob opened) and opens only the winner. It used to
|
|
72275
|
+
* walk `listByOwner`, which reads and base64-encodes every blob an owner
|
|
72276
|
+
* holds before the first comparison is made: measured on the live hub
|
|
72277
|
+
* 2026-08-30, a track's six kinds are 3 597 219 bytes and the tile serves
|
|
72278
|
+
* `thumbnailSmall` — 31 155 of them. An Events page of forty tiles read
|
|
72279
|
+
* ~144 MB and allocated ~190 MB of base64 to deliver ~1.2 MB, all of it on
|
|
72280
|
+
* the single-threaded post-analysis runner.
|
|
72281
|
+
*/
|
|
71755
72282
|
async readEventThumbnail(id, preferKind) {
|
|
71756
|
-
const
|
|
72283
|
+
const listInfo = (ownerKind, ownerId) => this.mediaStore?.listInfoByOwner(ownerKind, ownerId) ?? Promise.resolve([]);
|
|
72284
|
+
const getTrackIdForEvent = (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null);
|
|
72285
|
+
const readBlob = async (key) => {
|
|
72286
|
+
const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
|
|
72287
|
+
return file === null ? null : {
|
|
72288
|
+
bytes: Buffer.from(file.base64, "base64"),
|
|
72289
|
+
key: file.key
|
|
72290
|
+
};
|
|
72291
|
+
};
|
|
72292
|
+
const eventFiles = await listInfo("event", id);
|
|
71757
72293
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
71758
72294
|
const layers = await collectCropForcedMedia({
|
|
71759
|
-
listByOwner:
|
|
71760
|
-
getTrackIdForEvent
|
|
72295
|
+
listByOwner: listInfo,
|
|
72296
|
+
getTrackIdForEvent
|
|
71761
72297
|
}, id);
|
|
71762
|
-
const
|
|
71763
|
-
|
|
71764
|
-
return {
|
|
71765
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71766
|
-
key: chosen.key
|
|
71767
|
-
};
|
|
72298
|
+
const bodyKeys = new Set(layers.bodyFiles.map((f) => f.key));
|
|
72299
|
+
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
72300
|
}
|
|
71769
|
-
const
|
|
72301
|
+
const { candidates, pick } = await collectDefaultEventMedia({
|
|
71770
72302
|
eventId: id,
|
|
71771
72303
|
eventFiles,
|
|
71772
|
-
listTrackMedia: (trackId) =>
|
|
71773
|
-
getTrackIdForEvent
|
|
72304
|
+
listTrackMedia: (trackId) => listInfo("track", trackId),
|
|
72305
|
+
getTrackIdForEvent
|
|
71774
72306
|
});
|
|
71775
|
-
|
|
71776
|
-
return {
|
|
71777
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71778
|
-
key: chosen.key
|
|
71779
|
-
};
|
|
72307
|
+
return await readWinningBlob(candidates, pick, readBlob);
|
|
71780
72308
|
}
|
|
71781
72309
|
globalSettingsSchema(cap) {
|
|
71782
72310
|
return this.schema(buildGlobalSettingsSchema(cap));
|