@camstack/addon-post-analysis 1.2.150 → 1.2.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-BTviHGUC.js → dist-Bw6cidiJ.js} +360 -200
- package/dist/{dist-C8cA7PX_.mjs → dist-DSg_jDMt.mjs} +360 -200
- 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-DZGvAMuh.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DTO1HvN7.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DxVeMmnD.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DgkTmoHq.mjs → hostInit-r4RyRXk7.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +987 -535
- package/dist/pipeline-analytics/index.mjs +986 -534
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D8CtX8cU.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-Bw6cidiJ.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -5413,12 +5413,6 @@ function buildAlarmButtons(input) {
|
|
|
5413
5413
|
}];
|
|
5414
5414
|
}
|
|
5415
5415
|
//#endregion
|
|
5416
|
-
//#region src/notification-center/cap-caller.ts
|
|
5417
|
-
function requireCapCaller(method, caller) {
|
|
5418
|
-
if (caller === void 0 || typeof caller.userId !== "string" || caller.userId.length === 0) throw new Error(`notification-rules.${method} declares caller: 'required' and this call carried no caller identity — an addon-to-addon cap call has no authenticated user`);
|
|
5419
|
-
return caller;
|
|
5420
|
-
}
|
|
5421
|
-
//#endregion
|
|
5422
5416
|
//#region src/notification-center/audio-rule-matcher.ts
|
|
5423
5417
|
/**
|
|
5424
5418
|
* AudioWatcher — the Notification Center's matcher for audio rules
|
|
@@ -5661,6 +5655,12 @@ var AudioWatcher = class {
|
|
|
5661
5655
|
}
|
|
5662
5656
|
};
|
|
5663
5657
|
//#endregion
|
|
5658
|
+
//#region src/notification-center/cap-caller.ts
|
|
5659
|
+
function requireCapCaller(method, caller) {
|
|
5660
|
+
if (caller === void 0 || typeof caller.userId !== "string" || caller.userId.length === 0) throw new Error(`notification-rules.${method} declares caller: 'required' and this call carried no caller identity — an addon-to-addon cap call has no authenticated user`);
|
|
5661
|
+
return caller;
|
|
5662
|
+
}
|
|
5663
|
+
//#endregion
|
|
5664
5664
|
//#region src/shared/llm-vision/prompt-hygiene.ts
|
|
5665
5665
|
/**
|
|
5666
5666
|
* What a PIPELINE VALUE is allowed to look like once it is inside a prompt.
|
|
@@ -6270,12 +6270,107 @@ var NcDeviceStateCache = class {
|
|
|
6270
6270
|
}
|
|
6271
6271
|
};
|
|
6272
6272
|
//#endregion
|
|
6273
|
+
//#region src/notification-center/system-event-tap-url.ts
|
|
6274
|
+
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
6275
|
+
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
6276
|
+
var ADMIN_CLUSTER_PATH = "/system/cluster";
|
|
6277
|
+
var ADMIN_DEVICES_PATH = "/devices";
|
|
6278
|
+
/**
|
|
6279
|
+
* The query parameter naming a node.
|
|
6280
|
+
*
|
|
6281
|
+
* One spelling for both admin pages, mirrored by `NODE_PARAM` in
|
|
6282
|
+
* `packages/addon-admin-ui/src/lib/node-url-selection.ts` and pinned across
|
|
6283
|
+
* the two packages by `scripts/check-notification-tap-targets.ts`.
|
|
6284
|
+
*/
|
|
6285
|
+
var NODE_PARAM = "node";
|
|
6286
|
+
/** The device page's top-level tab parameter, read by `DeviceDetail`. */
|
|
6287
|
+
var TAB_PARAM = "tab";
|
|
6288
|
+
/** The Stream Broker tab id — a `WELL_KNOWN_TABS` member, hoisted `top-tab`. */
|
|
6289
|
+
var STREAM_BROKER_TAB = "stream-broker";
|
|
6290
|
+
/** `DeviceType.Camera`. Spelled here rather than imported so this stays a pure
|
|
6291
|
+
* string map with no device-model dependency; the guard pins it. */
|
|
6292
|
+
var CAMERA_DEVICE_TYPE = "camera";
|
|
6293
|
+
/** Kinds whose action is "go and apply an ADDON update". */
|
|
6294
|
+
var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
6295
|
+
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
6296
|
+
var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
|
|
6297
|
+
/**
|
|
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.
|
|
6308
|
+
*
|
|
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
|
|
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.
|
|
6316
|
+
*/
|
|
6317
|
+
var DEVICE_STREAM_KINDS = new Set([
|
|
6318
|
+
"device-offline",
|
|
6319
|
+
"device-online",
|
|
6320
|
+
"stream-offline",
|
|
6321
|
+
"stream-online",
|
|
6322
|
+
"camera-offline",
|
|
6323
|
+
"camera-online"
|
|
6324
|
+
]);
|
|
6325
|
+
/**
|
|
6326
|
+
* Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
|
|
6327
|
+
*
|
|
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.
|
|
6331
|
+
*/
|
|
6332
|
+
var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
|
|
6333
|
+
"stream-offline",
|
|
6334
|
+
"stream-online",
|
|
6335
|
+
"camera-offline",
|
|
6336
|
+
"camera-online"
|
|
6337
|
+
]);
|
|
6338
|
+
function withParam(path, key, value) {
|
|
6339
|
+
if (value === void 0 || value.length === 0) return path;
|
|
6340
|
+
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
6341
|
+
}
|
|
6342
|
+
/**
|
|
6343
|
+
* The admin path (with its query) a system event taps through to, or `null`
|
|
6344
|
+
* when the kind has no action.
|
|
6345
|
+
*
|
|
6346
|
+
* Exported for the cross-package guard and for tests that want the path
|
|
6347
|
+
* without a base; the dispatcher goes through {@link systemEventTapUrl}.
|
|
6348
|
+
*/
|
|
6349
|
+
function systemEventTapPath(subject) {
|
|
6350
|
+
if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
|
|
6351
|
+
if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
|
|
6352
|
+
if (DEVICE_STREAM_KINDS.has(subject.kind)) {
|
|
6353
|
+
const deviceId = subject.deviceId;
|
|
6354
|
+
if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
6355
|
+
const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
|
|
6356
|
+
return CAMERA_BY_CONSTRUCTION_KINDS.has(subject.kind) || subject.deviceType === CAMERA_DEVICE_TYPE ? withParam(devicePath, TAB_PARAM, STREAM_BROKER_TAB) : devicePath;
|
|
6357
|
+
}
|
|
6358
|
+
return null;
|
|
6359
|
+
}
|
|
6360
|
+
/** Full URL, or `null` when the kind has no action or no base is reachable. */
|
|
6361
|
+
function systemEventTapUrl(subject, baseUrl) {
|
|
6362
|
+
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
6363
|
+
const path = systemEventTapPath(subject);
|
|
6364
|
+
if (path === null) return null;
|
|
6365
|
+
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
6366
|
+
}
|
|
6367
|
+
//#endregion
|
|
6273
6368
|
//#region src/notification-center/default-tap-url.ts
|
|
6274
6369
|
/** The viewer's hosted base path under the hub. */
|
|
6275
6370
|
var VIEWER_BASE_PATH = "/viewer/camstack";
|
|
6276
6371
|
function defaultTapUrl(input) {
|
|
6277
|
-
if (input.systemEvent === true) return null;
|
|
6278
6372
|
if (input.baseUrl === null || input.baseUrl.length === 0) return null;
|
|
6373
|
+
if (input.systemEvent !== void 0) return systemEventTapUrl(input.systemEvent, input.baseUrl);
|
|
6279
6374
|
if (!Number.isInteger(input.deviceId) || input.deviceId <= 0) return null;
|
|
6280
6375
|
return `${input.baseUrl.replace(/\/+$/, "")}${VIEWER_BASE_PATH}/camera/${String(input.deviceId)}?at=${String(input.timestampMs)}`;
|
|
6281
6376
|
}
|
|
@@ -8868,13 +8963,14 @@ function trailAt(trail, timestamp) {
|
|
|
8868
8963
|
* cannot be asked (no trail, no polygon) or when nobody qualifies.
|
|
8869
8964
|
*/
|
|
8870
8965
|
function selectTriggerZoneCandidates(input) {
|
|
8871
|
-
const { candidates, trail, polygons } = input;
|
|
8966
|
+
const { candidates, trail, polygons, strict } = input;
|
|
8872
8967
|
if (candidates.length === 0 || trail.length === 0 || polygons.length === 0) return candidates;
|
|
8873
8968
|
const qualifying = candidates.filter((candidate) => {
|
|
8874
8969
|
const point = trailAt(trail, candidate.timestamp);
|
|
8875
8970
|
return point !== null && zoneOverlap(point, polygons) > 0;
|
|
8876
8971
|
});
|
|
8877
|
-
|
|
8972
|
+
if (qualifying.length > 0) return qualifying;
|
|
8973
|
+
return strict === true ? [] : candidates;
|
|
8878
8974
|
}
|
|
8879
8975
|
//#endregion
|
|
8880
8976
|
//#region src/notification-center/dispatcher.ts
|
|
@@ -8912,6 +9008,24 @@ var NC_STILL_WAIT_MS = 3e3;
|
|
|
8912
9008
|
* is the instant; the ladder then runs among the frames that are about it.
|
|
8913
9009
|
*/
|
|
8914
9010
|
var NC_TRIGGER_STILL_MAX_AGE_MS = TRAIL_MATCH_MAX_MS;
|
|
9011
|
+
/**
|
|
9012
|
+
* Whether this row's subject can still be photographed AGAIN after its trigger.
|
|
9013
|
+
*
|
|
9014
|
+
* The one thing that separates a row worth waiting for from a row whose only
|
|
9015
|
+
* frame already exists. A live visual track keeps writing media for as long as
|
|
9016
|
+
* it is open — its first frame, its periodic snapshots, its last frame all land
|
|
9017
|
+
* after the instant a zone condition fired. Every other record is photographed
|
|
9018
|
+
* once: an audio window, a device event and a package event are served by the
|
|
9019
|
+
* still shelf, which D178 makes take ONE picture for a whole burst, and a
|
|
9020
|
+
* `track-end` row is by definition past the last of them.
|
|
9021
|
+
*
|
|
9022
|
+
* Waiting on the wrong side of this line has cost both ways in production — 3 s
|
|
9023
|
+
* of latency on a scream when nothing more was coming, and an empty doorstep on
|
|
9024
|
+
* 'Persona su Uscio' when something was.
|
|
9025
|
+
*/
|
|
9026
|
+
function canStillGainMedia(entry) {
|
|
9027
|
+
return entry.recordKind === "object-event";
|
|
9028
|
+
}
|
|
8915
9029
|
var NcDispatcher = class {
|
|
8916
9030
|
deps;
|
|
8917
9031
|
targetCache = null;
|
|
@@ -9391,7 +9505,12 @@ var NcDispatcher = class {
|
|
|
9391
9505
|
deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
|
|
9392
9506
|
timestampMs: subject.timestamp,
|
|
9393
9507
|
baseUrl: await this.deps.viewerUrlBase().catch(() => null),
|
|
9394
|
-
systemEvent
|
|
9508
|
+
...systemEvent !== void 0 ? { systemEvent: {
|
|
9509
|
+
kind: systemEvent.kind,
|
|
9510
|
+
...systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {},
|
|
9511
|
+
...systemEvent.deviceType !== void 0 ? { deviceType: systemEvent.deviceType } : {},
|
|
9512
|
+
...systemEvent.nodeId !== void 0 ? { nodeId: systemEvent.nodeId } : {}
|
|
9513
|
+
} } : {}
|
|
9395
9514
|
}) : null;
|
|
9396
9515
|
const actions = await this.mintActions(entry, target);
|
|
9397
9516
|
return {
|
|
@@ -9526,7 +9645,7 @@ var NcDispatcher = class {
|
|
|
9526
9645
|
const out = [];
|
|
9527
9646
|
const zoneIdsWanted = entry.payload.mediaZoneIds;
|
|
9528
9647
|
const stillOverride = zoneIdsWanted !== void 0 && zoneIdsWanted.length > 0 ? "keyFrame" : void 0;
|
|
9529
|
-
const firstLook = await this.resolveAttachment(entry, stillOverride, "any");
|
|
9648
|
+
const firstLook = await this.resolveAttachment(entry, stillOverride, canStillGainMedia(entry) ? "trigger-only" : "any");
|
|
9530
9649
|
const footagePending = this.renderFootage(entry);
|
|
9531
9650
|
const still = firstLook !== null || entry.payload.media === "none" ? firstLook : await this.waitForStill(entry, stillOverride);
|
|
9532
9651
|
const footage = await footagePending;
|
|
@@ -9770,20 +9889,34 @@ var NcDispatcher = class {
|
|
|
9770
9889
|
* throw. And when nothing qualifies, the full list comes back: a real picture
|
|
9771
9890
|
* beats no picture (the same rule D212 settled for the best shot).
|
|
9772
9891
|
*/
|
|
9773
|
-
async narrowToTriggerZone(entry, ordered) {
|
|
9892
|
+
async narrowToTriggerZone(entry, ordered, freshness) {
|
|
9774
9893
|
const zoneIds = entry.payload.triggerZoneIds;
|
|
9775
9894
|
const trackId = entry.payload.subject.trackId;
|
|
9776
9895
|
const getTrail = this.deps.getSubjectTrail;
|
|
9777
9896
|
const deviceId = entry.payload.subject.deviceId;
|
|
9778
|
-
|
|
9897
|
+
const strict = freshness === "trigger-only";
|
|
9898
|
+
if (zoneIds === void 0 || zoneIds.length === 0 || trackId === void 0 || getTrail === void 0 || ordered.length < 2 && !strict) return ordered;
|
|
9779
9899
|
try {
|
|
9780
9900
|
const [polygons, trail] = await Promise.all([this.deps.getZonePolygons?.(deviceId, zoneIds) ?? Promise.resolve([]), getTrail(deviceId, trackId)]);
|
|
9781
9901
|
const kept = selectTriggerZoneCandidates({
|
|
9782
9902
|
candidates: ordered,
|
|
9783
9903
|
trail,
|
|
9784
9904
|
polygons,
|
|
9785
|
-
triggerAt: entry.payload.subject.timestamp
|
|
9905
|
+
triggerAt: entry.payload.subject.timestamp,
|
|
9906
|
+
strict
|
|
9786
9907
|
});
|
|
9908
|
+
if (kept.length === 0) {
|
|
9909
|
+
this.deps.logger.debug("no still from the zone that fired the rule — still waiting", {
|
|
9910
|
+
tags: { deviceId },
|
|
9911
|
+
meta: {
|
|
9912
|
+
ruleId: entry.ruleId,
|
|
9913
|
+
trackId,
|
|
9914
|
+
zoneIds: zoneIds.join(","),
|
|
9915
|
+
rejected: ordered.length
|
|
9916
|
+
}
|
|
9917
|
+
});
|
|
9918
|
+
return kept;
|
|
9919
|
+
}
|
|
9787
9920
|
if (kept.length === ordered.length) return kept;
|
|
9788
9921
|
this.deps.logger.info("still narrowed to the zone that fired the rule", {
|
|
9789
9922
|
tags: { deviceId },
|
|
@@ -9863,7 +9996,8 @@ var NcDispatcher = class {
|
|
|
9863
9996
|
for (const kind of preference) for (const file of files) if (file.kind === kind) ordered.push(file);
|
|
9864
9997
|
const inBand = this.narrowToTriggerInstant(entry, ordered, freshness);
|
|
9865
9998
|
if (inBand.length === 0) continue;
|
|
9866
|
-
const shortlist = await this.narrowToTriggerZone(entry, inBand);
|
|
9999
|
+
const shortlist = await this.narrowToTriggerZone(entry, inBand, freshness);
|
|
10000
|
+
if (shortlist.length === 0) continue;
|
|
9867
10001
|
for (const file of shortlist) {
|
|
9868
10002
|
const raw = Buffer.from(file.base64, "base64");
|
|
9869
10003
|
if (raw.byteLength === 0) continue;
|
|
@@ -10617,16 +10751,26 @@ function incomingFromUpdateAvailable(facts, data) {
|
|
|
10617
10751
|
...nodeIds.length > 0 ? { nodeIds } : {}
|
|
10618
10752
|
});
|
|
10619
10753
|
}
|
|
10620
|
-
/**
|
|
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
|
+
*/
|
|
10621
10765
|
function parseAddonUpdateComplete(data) {
|
|
10622
10766
|
const packageName = stringField(data, "packageName");
|
|
10623
10767
|
const fromVersion = stringField(data, "fromVersion");
|
|
10624
10768
|
const toVersion = stringField(data, "toVersion");
|
|
10625
|
-
if (packageName === void 0 ||
|
|
10769
|
+
if (packageName === void 0 || toVersion === void 0) return null;
|
|
10626
10770
|
const nodeId = stringField(data, "agent") ?? stringField(data, "nodeId");
|
|
10627
10771
|
return {
|
|
10628
10772
|
packageName,
|
|
10629
|
-
fromVersion,
|
|
10773
|
+
...fromVersion !== void 0 ? { fromVersion } : {},
|
|
10630
10774
|
toVersion,
|
|
10631
10775
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10632
10776
|
};
|
|
@@ -10636,7 +10780,10 @@ function incomingFromAddonUpdates(facts, updates) {
|
|
|
10636
10780
|
const packageNames = unique(updates.map((update) => update.packageName));
|
|
10637
10781
|
const nodeIds = unique(updates.map((update) => update.nodeId).filter((id) => id !== void 0));
|
|
10638
10782
|
const signature = updates.map((update) => `${update.nodeId ?? ""}:${update.packageName}@${update.toVersion}`).toSorted().join(",");
|
|
10639
|
-
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");
|
|
10640
10787
|
const head = updates[0];
|
|
10641
10788
|
return systemSubject({
|
|
10642
10789
|
facts,
|
|
@@ -23473,17 +23620,17 @@ function planRuleTest(input) {
|
|
|
23473
23620
|
deviceName: `camera ${String(systemScope.deviceId ?? deviceId)}`,
|
|
23474
23621
|
nodeId: systemScope.nodeId ?? "hub",
|
|
23475
23622
|
packageName: systemScope.packageName ?? "@camstack/server",
|
|
23476
|
-
currentVersion: "
|
|
23477
|
-
latestVersion: "
|
|
23623
|
+
currentVersion: "1.0.0",
|
|
23624
|
+
latestVersion: "1.0.1",
|
|
23478
23625
|
profile: "low",
|
|
23479
23626
|
seconds: "30",
|
|
23480
23627
|
devices: "cuda:0",
|
|
23481
23628
|
minutes: "15",
|
|
23482
23629
|
addons: TEST_CRASHED_RUNNER,
|
|
23483
|
-
list: "@camstack/addon-pipeline 1.0.
|
|
23630
|
+
list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
|
|
23484
23631
|
nodes: "hub",
|
|
23485
|
-
fromVersion: "
|
|
23486
|
-
toVersion: "
|
|
23632
|
+
fromVersion: "1.0.0",
|
|
23633
|
+
toVersion: "1.0.1",
|
|
23487
23634
|
title: "Export",
|
|
23488
23635
|
count: "1"
|
|
23489
23636
|
},
|
|
@@ -25509,13 +25656,46 @@ var MEDIA_COLUMNS = [
|
|
|
25509
25656
|
type: "TEXT"
|
|
25510
25657
|
})
|
|
25511
25658
|
];
|
|
25512
|
-
var MEDIA_INDEXES = [
|
|
25513
|
-
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
25517
|
-
|
|
25518
|
-
|
|
25659
|
+
var MEDIA_INDEXES = [
|
|
25660
|
+
{
|
|
25661
|
+
name: "idx_media_owner",
|
|
25662
|
+
columns: ["ownerKind", "ownerId"]
|
|
25663
|
+
},
|
|
25664
|
+
{
|
|
25665
|
+
name: "idx_media_device_ts",
|
|
25666
|
+
columns: ["deviceId", "timestamp"]
|
|
25667
|
+
},
|
|
25668
|
+
(
|
|
25669
|
+
/**
|
|
25670
|
+
* The `locationId IS NULL` seal gate — see `media-relocate-engine.ts`.
|
|
25671
|
+
*
|
|
25672
|
+
* NULLs sort FIRST in a SQLite index, so `WHERE locationId IS NULL` is a
|
|
25673
|
+
* contiguous range from the head of the b-tree: its cost is proportional to
|
|
25674
|
+
* the ANSWER, not to the table. That is the property the gate needs. The
|
|
25675
|
+
* question it must answer fastest is "are there none left", and with this
|
|
25676
|
+
* index that is one seek that immediately falls off the end of an empty
|
|
25677
|
+
* range — regardless of how many million stamped rows sit behind it.
|
|
25678
|
+
*
|
|
25679
|
+
* **What it costs.** One extra b-tree maintained by every insert into a
|
|
25680
|
+
* table taking ~92 k rows/day, and roughly 25 bytes of index per row (~30 MB
|
|
25681
|
+
* at 1.25 M rows). At ~1 insert/s sustained that maintenance is noise
|
|
25682
|
+
* against the JPEG write the same call performs. The one-off cost is real
|
|
25683
|
+
* and worth naming: the FIRST declare after this ships builds the index over
|
|
25684
|
+
* every existing row, on a recordings volume that is currently saturated —
|
|
25685
|
+
* expect the analytics addon's boot to stall for tens of seconds to a couple
|
|
25686
|
+
* of minutes, once.
|
|
25687
|
+
*
|
|
25688
|
+
* A PARTIAL index (`WHERE locationId IS NULL`) would be smaller still and
|
|
25689
|
+
* would cost nothing at all once sealed. It is not used because
|
|
25690
|
+
* `CollectionIndexSchema` has no predicate and the schema fingerprint does
|
|
25691
|
+
* not hash one — adding both to serve one query is a bigger change than the
|
|
25692
|
+
* query deserves, and the plain index already makes the gate O(log n).
|
|
25693
|
+
*/
|
|
25694
|
+
{
|
|
25695
|
+
name: "idx_media_location",
|
|
25696
|
+
columns: ["locationId"]
|
|
25697
|
+
})
|
|
25698
|
+
];
|
|
25519
25699
|
/** The storage location of a media row/record: its stamped `locationId`, or
|
|
25520
25700
|
* the default `eventMedia` location for NULL (pre-multi-location) rows. */
|
|
25521
25701
|
var DEFAULT_MEDIA_LOCATION = "eventMedia";
|
|
@@ -25600,7 +25780,7 @@ var MediaStore = class {
|
|
|
25600
25780
|
});
|
|
25601
25781
|
}
|
|
25602
25782
|
/** Persist a media blob and register its metadata row. Returns the
|
|
25603
|
-
* collection key (
|
|
25783
|
+
* collection key (MediaFileRef.key) that callers store as reference. */
|
|
25604
25784
|
async put(params) {
|
|
25605
25785
|
if (this.writeGate) return this.writeGate.run(() => this.putUnchecked(params));
|
|
25606
25786
|
return this.putUnchecked(params);
|
|
@@ -25922,8 +26102,20 @@ var MediaStore = class {
|
|
|
25922
26102
|
* its device at write time, so scoping the query by it makes a media read
|
|
25923
26103
|
* for a device the caller cannot name return nothing — the DB itself
|
|
25924
26104
|
* enforces the boundary, no post-hoc ownership check to forget.
|
|
26105
|
+
*
|
|
26106
|
+
* `kinds` is the WORK filter, and it belongs here rather than on the answer.
|
|
26107
|
+
* Every row this returns has had its blob read off disk and base64-encoded,
|
|
26108
|
+
* so a caller that filters afterwards has already paid for every kind it is
|
|
26109
|
+
* about to discard: measured on the live hub, one track's six kinds are
|
|
26110
|
+
* 3 597 219 bytes and `thumbnailSmall` is 31 155 of them, and asking for the
|
|
26111
|
+
* small one alone cost exactly as much as asking for all six. `kind` is a
|
|
26112
|
+
* real column (see {@link MEDIA_COLUMNS}), so the narrowing happens in the
|
|
26113
|
+
* query and the blobs of the other kinds are never opened.
|
|
26114
|
+
*
|
|
26115
|
+
* Absent (or empty) means every kind — the back-compat default, and the
|
|
26116
|
+
* honest answer for a caller that has not decided what it needs.
|
|
25925
26117
|
*/
|
|
25926
|
-
async listByOwner(ownerKind, ownerId, deviceId) {
|
|
26118
|
+
async listByOwner(ownerKind, ownerId, deviceId, kinds) {
|
|
25927
26119
|
const rows = await this.store.query.query({
|
|
25928
26120
|
collection: MEDIA_COLLECTION,
|
|
25929
26121
|
filter: {
|
|
@@ -25935,6 +26127,7 @@ var MediaStore = class {
|
|
|
25935
26127
|
ownerId,
|
|
25936
26128
|
deviceId
|
|
25937
26129
|
},
|
|
26130
|
+
...kinds !== void 0 && kinds.length > 0 ? { whereIn: { kind: [...kinds] } } : {},
|
|
25938
26131
|
orderBy: {
|
|
25939
26132
|
field: "timestamp",
|
|
25940
26133
|
direction: "asc"
|
|
@@ -27851,6 +28044,63 @@ var embeddingActions = require_dist.defineCustomActions({
|
|
|
27851
28044
|
"embedding.reembedFacesStatus": require_dist.customAction(NoInputSchema$1, ReembedFacesStatusSchema)
|
|
27852
28045
|
});
|
|
27853
28046
|
//#endregion
|
|
28047
|
+
//#region src/pipeline-analytics/media-file-url.ts
|
|
28048
|
+
/**
|
|
28049
|
+
* The plane path for one stored media key.
|
|
28050
|
+
*
|
|
28051
|
+
* `prefix` is `/addon/<addonId>/event-media` — a pure function of the addon
|
|
28052
|
+
* id, so it exists whether or not `dataPlane.serve` succeeded. A row that
|
|
28053
|
+
* named no address at all would be the silent drop this repo keeps paying for:
|
|
28054
|
+
* a client cannot tell "no bytes" from "no plane", and renders neither.
|
|
28055
|
+
*/
|
|
28056
|
+
function mediaFileUrl(prefix, key) {
|
|
28057
|
+
return `${prefix}/${encodeURIComponent(key)}`;
|
|
28058
|
+
}
|
|
28059
|
+
/**
|
|
28060
|
+
* A stored row as the wire sees it: the ref, the address, and — transitionally
|
|
28061
|
+
* — the bytes.
|
|
28062
|
+
*
|
|
28063
|
+
* `base64` is copied through ONLY because the deployed viewer's track-detail
|
|
28064
|
+
* hero still requires the field to parse a row at all. See the `MediaFile`
|
|
28065
|
+
* docblock in `pipeline-analytics.cap.ts`; deleting the field is a one-line
|
|
28066
|
+
* change here and one in the schema.
|
|
28067
|
+
*
|
|
28068
|
+
* `provenance` is deliberately NOT spread: it is store-internal, and the
|
|
28069
|
+
* previous `{ ...row, kind }` shape published it by accident.
|
|
28070
|
+
*/
|
|
28071
|
+
function toWireMediaFile(row, prefix) {
|
|
28072
|
+
return {
|
|
28073
|
+
key: row.key,
|
|
28074
|
+
kind: row.kind,
|
|
28075
|
+
sizeBytes: row.sizeBytes,
|
|
28076
|
+
timestamp: row.timestamp,
|
|
28077
|
+
url: mediaFileUrl(prefix, row.key),
|
|
28078
|
+
base64: row.base64
|
|
28079
|
+
};
|
|
28080
|
+
}
|
|
28081
|
+
/** The same row without its bytes — the manifest shape. Carries the SAME
|
|
28082
|
+
* `url` as the byte read, so a client never builds the plane path itself. */
|
|
28083
|
+
function toWireMediaFileInfo(row, prefix) {
|
|
28084
|
+
return {
|
|
28085
|
+
key: row.key,
|
|
28086
|
+
kind: row.kind,
|
|
28087
|
+
sizeBytes: row.sizeBytes,
|
|
28088
|
+
timestamp: row.timestamp,
|
|
28089
|
+
url: mediaFileUrl(prefix, row.key)
|
|
28090
|
+
};
|
|
28091
|
+
}
|
|
28092
|
+
//#endregion
|
|
28093
|
+
//#region src/pipeline-analytics/gallery-media-row.ts
|
|
28094
|
+
function toGalleryMediaRow(row, prefix) {
|
|
28095
|
+
return {
|
|
28096
|
+
key: row.key,
|
|
28097
|
+
kind: row.kind,
|
|
28098
|
+
url: mediaFileUrl(prefix, row.key),
|
|
28099
|
+
sizeBytes: row.sizeBytes,
|
|
28100
|
+
timestamp: row.timestamp
|
|
28101
|
+
};
|
|
28102
|
+
}
|
|
28103
|
+
//#endregion
|
|
27854
28104
|
//#region src/pipeline-analytics/include-crops-default.ts
|
|
27855
28105
|
function resolveIncludeCrops(input) {
|
|
27856
28106
|
return input?.includeCrops === true;
|
|
@@ -29510,6 +29760,7 @@ var FaceGalleryProvider = class {
|
|
|
29510
29760
|
logger;
|
|
29511
29761
|
refreshGallery;
|
|
29512
29762
|
eventMediaBaseUrl;
|
|
29763
|
+
eventMediaPathPrefix;
|
|
29513
29764
|
emitFaceGalleryChanged;
|
|
29514
29765
|
constructor(deps) {
|
|
29515
29766
|
this.identityStore = deps.identityStore;
|
|
@@ -29520,6 +29771,7 @@ var FaceGalleryProvider = class {
|
|
|
29520
29771
|
this.logger = deps.logger;
|
|
29521
29772
|
this.refreshGallery = deps.refreshGallery;
|
|
29522
29773
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
29774
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
29523
29775
|
this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
|
|
29524
29776
|
}
|
|
29525
29777
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
@@ -29707,13 +29959,7 @@ var FaceGalleryProvider = class {
|
|
|
29707
29959
|
* the enrolled identity copies — those live under identity owner).
|
|
29708
29960
|
*/
|
|
29709
29961
|
async getFaceMedia(input) {
|
|
29710
|
-
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => (
|
|
29711
|
-
key: m.key,
|
|
29712
|
-
kind: m.kind,
|
|
29713
|
-
base64: m.base64,
|
|
29714
|
-
sizeBytes: m.sizeBytes,
|
|
29715
|
-
timestamp: m.timestamp
|
|
29716
|
-
}));
|
|
29962
|
+
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
29717
29963
|
}
|
|
29718
29964
|
/**
|
|
29719
29965
|
* Assign a buffered face to an identity:
|
|
@@ -32426,209 +32672,6 @@ var FrameProcessor = class {
|
|
|
32426
32672
|
};
|
|
32427
32673
|
}
|
|
32428
32674
|
};
|
|
32429
|
-
//#endregion
|
|
32430
|
-
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
32431
|
-
/**
|
|
32432
|
-
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
32433
|
-
* return the tracks it produced.
|
|
32434
|
-
*
|
|
32435
|
-
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
32436
|
-
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
32437
|
-
*/
|
|
32438
|
-
function runFrameProcessorPass(input) {
|
|
32439
|
-
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
32440
|
-
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
32441
|
-
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
32442
|
-
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
32443
|
-
const accum = /* @__PURE__ */ new Map();
|
|
32444
|
-
for (const { timestamp, frame } of input.frames) {
|
|
32445
|
-
const result = processor.process({
|
|
32446
|
-
timestamp,
|
|
32447
|
-
frame
|
|
32448
|
-
});
|
|
32449
|
-
for (const td of result.tracked) {
|
|
32450
|
-
const existing = accum.get(td.trackId);
|
|
32451
|
-
const matched = td.matchedThisFrame ? 1 : 0;
|
|
32452
|
-
if (existing === void 0) accum.set(td.trackId, {
|
|
32453
|
-
className: td.className,
|
|
32454
|
-
firstSeenMs: timestamp,
|
|
32455
|
-
lastSeenMs: timestamp,
|
|
32456
|
-
bbox: { ...td.bbox },
|
|
32457
|
-
framesMatched: matched
|
|
32458
|
-
});
|
|
32459
|
-
else {
|
|
32460
|
-
existing.className = td.className;
|
|
32461
|
-
existing.lastSeenMs = timestamp;
|
|
32462
|
-
existing.framesMatched += matched;
|
|
32463
|
-
}
|
|
32464
|
-
}
|
|
32465
|
-
}
|
|
32466
|
-
return { tracks: [...accum.values()].map((t) => ({
|
|
32467
|
-
className: t.className,
|
|
32468
|
-
firstSeenMs: t.firstSeenMs,
|
|
32469
|
-
lastSeenMs: t.lastSeenMs,
|
|
32470
|
-
bbox: t.bbox,
|
|
32471
|
-
framesMatched: t.framesMatched
|
|
32472
|
-
})) };
|
|
32473
|
-
}
|
|
32474
|
-
function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, score) {
|
|
32475
|
-
if (frameWidth <= 0 || frameHeight <= 0) return true;
|
|
32476
|
-
if (score !== void 0 && score >= .5) return true;
|
|
32477
|
-
if (bbox.w * bbox.h / (frameWidth * frameHeight) >= .85) return false;
|
|
32478
|
-
if (bbox.h >= frameHeight * .95) return false;
|
|
32479
|
-
if (bbox.w >= frameWidth * .95) return false;
|
|
32480
|
-
return true;
|
|
32481
|
-
}
|
|
32482
|
-
/**
|
|
32483
|
-
* Minimum fraction of the track's BEST-SEEN bbox area a retry frame's bbox must
|
|
32484
|
-
* still cover to be an acceptable best-`thumbnail` RETRY subject. 0.45 tolerates
|
|
32485
|
-
* normal breathing of the tracker box (~2/3 per side) while rejecting the
|
|
32486
|
-
* degenerate shrink (observed live 2026-07-22: a car born at 78px retried at
|
|
32487
|
-
* 26px once the native lease warmed — a ~0.11 area fraction — and the landed
|
|
32488
|
-
* "best" thumbnail was a sliver of a distant car).
|
|
32489
|
-
*/
|
|
32490
|
-
var RETRY_MIN_FRACTION_OF_BEST = .45;
|
|
32491
|
-
/**
|
|
32492
|
-
* RETRY-ONLY gate (#27-A degenerate-retry fix): on a cold on-motion session the
|
|
32493
|
-
* native lease misses the first seconds, so the large-subject captures return
|
|
32494
|
-
* null and the per-frame retry re-fires with the CURRENT frame's bbox. By the
|
|
32495
|
-
* time the lease warms the subject may have shrunk to a sliver; capturing THAT
|
|
32496
|
-
* frame lands a genuine-native-but-useless thumbnail and `thumbnailLanded`
|
|
32497
|
-
* stops all further retries. Reject a retry whose current bbox area has
|
|
32498
|
-
* collapsed below `minFractionOfBest` of the track's best-seen bbox area.
|
|
32499
|
-
*
|
|
32500
|
-
* Applies ONLY to the `!thumbnailLanded` retry path — a genuine new-best
|
|
32501
|
-
* capture is never routed through this gate (the caller bypasses it on
|
|
32502
|
-
* `isNewBest`). With no best-seen reference (or a degenerate one) there is no
|
|
32503
|
-
* info to reject on → plausible.
|
|
32504
|
-
*/
|
|
32505
|
-
function isPlausibleRetryBox(current, bestSeen, minFractionOfBest = RETRY_MIN_FRACTION_OF_BEST) {
|
|
32506
|
-
if (bestSeen === null) return true;
|
|
32507
|
-
const bestArea = bestSeen.w * bestSeen.h;
|
|
32508
|
-
if (bestArea <= 0) return true;
|
|
32509
|
-
return current.w * current.h >= minFractionOfBest * bestArea;
|
|
32510
|
-
}
|
|
32511
|
-
//#endregion
|
|
32512
|
-
//#region src/pipeline-analytics/pipeline/semantic-promotion.ts
|
|
32513
|
-
/**
|
|
32514
|
-
* Compose the {@link SemanticRecognitionFrame} for a plate read / accepted
|
|
32515
|
-
* fine classification. ONE rule, enforced here rather than at the call site:
|
|
32516
|
-
* the promoted `bbox` is ALWAYS the SUBJECT box (`frame.bbox`), never the
|
|
32517
|
-
* detail box — see {@link DetailAttributionResult.bbox} for what feeding the
|
|
32518
|
-
* detail box cost. The face path (`face-recognizer.ts` `onSemanticMatch`)
|
|
32519
|
-
* already passes its body box; this keeps the plate/classification path on the
|
|
32520
|
-
* same discipline.
|
|
32521
|
-
*/
|
|
32522
|
-
function semanticFrameForDetailAttribution(deviceId, trackId, detail, frame) {
|
|
32523
|
-
return {
|
|
32524
|
-
deviceId,
|
|
32525
|
-
trackId,
|
|
32526
|
-
timestamp: frame.timestamp,
|
|
32527
|
-
bbox: {
|
|
32528
|
-
x: frame.bbox.x,
|
|
32529
|
-
y: frame.bbox.y,
|
|
32530
|
-
w: frame.bbox.w,
|
|
32531
|
-
h: frame.bbox.h
|
|
32532
|
-
},
|
|
32533
|
-
frameWidth: frame.frameWidth,
|
|
32534
|
-
frameHeight: frame.frameHeight,
|
|
32535
|
-
kind: detail.className === "plate" ? "plate" : "classification",
|
|
32536
|
-
score: detail.score,
|
|
32537
|
-
confidence: frame.confidence,
|
|
32538
|
-
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
32539
|
-
};
|
|
32540
|
-
}
|
|
32541
|
-
/**
|
|
32542
|
-
* Offer the recognition frame to the best-frame ranking with the semantic
|
|
32543
|
-
* tier. On a win: record the best-seen bbox (plausible boxes only — an
|
|
32544
|
-
* envelope-exploded box must not poison the retry reference, mirroring
|
|
32545
|
-
* `buildSnapshotTargets`) and re-fire the captures when the pixels are still
|
|
32546
|
-
* reachable.
|
|
32547
|
-
*/
|
|
32548
|
-
function promoteSemanticBestFrame(deps, input) {
|
|
32549
|
-
const { deviceId, trackId, bbox, frameWidth, frameHeight, timestamp } = input;
|
|
32550
|
-
if (!deps.observeBestFrame({
|
|
32551
|
-
deviceId,
|
|
32552
|
-
trackId,
|
|
32553
|
-
confidence: input.confidence,
|
|
32554
|
-
timestamp,
|
|
32555
|
-
edgeClear: isEdgeClear({
|
|
32556
|
-
bbox,
|
|
32557
|
-
frameWidth,
|
|
32558
|
-
frameHeight
|
|
32559
|
-
}),
|
|
32560
|
-
centerScore: bboxCenterScore(bbox, frameWidth, frameHeight),
|
|
32561
|
-
containment: bboxContainment(bbox, frameWidth, frameHeight),
|
|
32562
|
-
sizeScore: bboxSizeScore(bbox, frameWidth, frameHeight),
|
|
32563
|
-
squareFraction: centralSquareContainmentFraction(bbox, {
|
|
32564
|
-
W: frameWidth,
|
|
32565
|
-
H: frameHeight
|
|
32566
|
-
}),
|
|
32567
|
-
semantic: true
|
|
32568
|
-
})) {
|
|
32569
|
-
deps.logger.debug("best-shot: semantic frame did not beat the held peak", {
|
|
32570
|
-
tags: { deviceId },
|
|
32571
|
-
meta: {
|
|
32572
|
-
trackId,
|
|
32573
|
-
kind: input.kind,
|
|
32574
|
-
score: input.score,
|
|
32575
|
-
timestamp
|
|
32576
|
-
}
|
|
32577
|
-
});
|
|
32578
|
-
return {
|
|
32579
|
-
promoted: false,
|
|
32580
|
-
captured: false
|
|
32581
|
-
};
|
|
32582
|
-
}
|
|
32583
|
-
const plausible = isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, input.confidence);
|
|
32584
|
-
if (plausible) deps.recordBestSeenBbox(deviceId, trackId, {
|
|
32585
|
-
x: bbox.x,
|
|
32586
|
-
y: bbox.y,
|
|
32587
|
-
w: bbox.w,
|
|
32588
|
-
h: bbox.h,
|
|
32589
|
-
frameWidth,
|
|
32590
|
-
frameHeight,
|
|
32591
|
-
timestamp
|
|
32592
|
-
});
|
|
32593
|
-
if (!plausible || input.frameHandle === void 0) {
|
|
32594
|
-
deps.logger.info("best-shot: semantic promotion without capture", {
|
|
32595
|
-
tags: { deviceId },
|
|
32596
|
-
meta: {
|
|
32597
|
-
trackId,
|
|
32598
|
-
kind: input.kind,
|
|
32599
|
-
score: input.score,
|
|
32600
|
-
timestamp,
|
|
32601
|
-
reason: plausible ? "no frame handle" : "implausible box"
|
|
32602
|
-
}
|
|
32603
|
-
});
|
|
32604
|
-
return {
|
|
32605
|
-
promoted: true,
|
|
32606
|
-
captured: false
|
|
32607
|
-
};
|
|
32608
|
-
}
|
|
32609
|
-
deps.captureBestShot({
|
|
32610
|
-
deviceId,
|
|
32611
|
-
trackId,
|
|
32612
|
-
timestamp,
|
|
32613
|
-
bbox,
|
|
32614
|
-
frameWidth,
|
|
32615
|
-
frameHeight,
|
|
32616
|
-
frameHandle: input.frameHandle
|
|
32617
|
-
});
|
|
32618
|
-
deps.logger.info("best-shot: semantic promotion", {
|
|
32619
|
-
tags: { deviceId },
|
|
32620
|
-
meta: {
|
|
32621
|
-
trackId,
|
|
32622
|
-
kind: input.kind,
|
|
32623
|
-
score: input.score,
|
|
32624
|
-
timestamp
|
|
32625
|
-
}
|
|
32626
|
-
});
|
|
32627
|
-
return {
|
|
32628
|
-
promoted: true,
|
|
32629
|
-
captured: true
|
|
32630
|
-
};
|
|
32631
|
-
}
|
|
32632
32675
|
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
32633
32676
|
* evicts the oldest record. */
|
|
32634
32677
|
var PendingPreviewUpgrades = class {
|
|
@@ -32760,6 +32803,164 @@ async function runPreviewUpgrade(deps, input) {
|
|
|
32760
32803
|
});
|
|
32761
32804
|
return "upgraded";
|
|
32762
32805
|
}
|
|
32806
|
+
function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, score) {
|
|
32807
|
+
if (frameWidth <= 0 || frameHeight <= 0) return true;
|
|
32808
|
+
if (score !== void 0 && score >= .5) return true;
|
|
32809
|
+
if (bbox.w * bbox.h / (frameWidth * frameHeight) >= .85) return false;
|
|
32810
|
+
if (bbox.h >= frameHeight * .95) return false;
|
|
32811
|
+
if (bbox.w >= frameWidth * .95) return false;
|
|
32812
|
+
return true;
|
|
32813
|
+
}
|
|
32814
|
+
/**
|
|
32815
|
+
* Minimum fraction of the track's BEST-SEEN bbox area a retry frame's bbox must
|
|
32816
|
+
* still cover to be an acceptable best-`thumbnail` RETRY subject. 0.45 tolerates
|
|
32817
|
+
* normal breathing of the tracker box (~2/3 per side) while rejecting the
|
|
32818
|
+
* degenerate shrink (observed live 2026-07-22: a car born at 78px retried at
|
|
32819
|
+
* 26px once the native lease warmed — a ~0.11 area fraction — and the landed
|
|
32820
|
+
* "best" thumbnail was a sliver of a distant car).
|
|
32821
|
+
*/
|
|
32822
|
+
var RETRY_MIN_FRACTION_OF_BEST = .45;
|
|
32823
|
+
/**
|
|
32824
|
+
* RETRY-ONLY gate (#27-A degenerate-retry fix): on a cold on-motion session the
|
|
32825
|
+
* native lease misses the first seconds, so the large-subject captures return
|
|
32826
|
+
* null and the per-frame retry re-fires with the CURRENT frame's bbox. By the
|
|
32827
|
+
* time the lease warms the subject may have shrunk to a sliver; capturing THAT
|
|
32828
|
+
* frame lands a genuine-native-but-useless thumbnail and `thumbnailLanded`
|
|
32829
|
+
* stops all further retries. Reject a retry whose current bbox area has
|
|
32830
|
+
* collapsed below `minFractionOfBest` of the track's best-seen bbox area.
|
|
32831
|
+
*
|
|
32832
|
+
* Applies ONLY to the `!thumbnailLanded` retry path — a genuine new-best
|
|
32833
|
+
* capture is never routed through this gate (the caller bypasses it on
|
|
32834
|
+
* `isNewBest`). With no best-seen reference (or a degenerate one) there is no
|
|
32835
|
+
* info to reject on → plausible.
|
|
32836
|
+
*/
|
|
32837
|
+
function isPlausibleRetryBox(current, bestSeen, minFractionOfBest = RETRY_MIN_FRACTION_OF_BEST) {
|
|
32838
|
+
if (bestSeen === null) return true;
|
|
32839
|
+
const bestArea = bestSeen.w * bestSeen.h;
|
|
32840
|
+
if (bestArea <= 0) return true;
|
|
32841
|
+
return current.w * current.h >= minFractionOfBest * bestArea;
|
|
32842
|
+
}
|
|
32843
|
+
//#endregion
|
|
32844
|
+
//#region src/pipeline-analytics/pipeline/semantic-promotion.ts
|
|
32845
|
+
/**
|
|
32846
|
+
* Compose the {@link SemanticRecognitionFrame} for a plate read / accepted
|
|
32847
|
+
* fine classification. ONE rule, enforced here rather than at the call site:
|
|
32848
|
+
* the promoted `bbox` is ALWAYS the SUBJECT box (`frame.bbox`), never the
|
|
32849
|
+
* detail box — see {@link DetailAttributionResult.bbox} for what feeding the
|
|
32850
|
+
* detail box cost. The face path (`face-recognizer.ts` `onSemanticMatch`)
|
|
32851
|
+
* already passes its body box; this keeps the plate/classification path on the
|
|
32852
|
+
* same discipline.
|
|
32853
|
+
*/
|
|
32854
|
+
function semanticFrameForDetailAttribution(deviceId, trackId, detail, frame) {
|
|
32855
|
+
return {
|
|
32856
|
+
deviceId,
|
|
32857
|
+
trackId,
|
|
32858
|
+
timestamp: frame.timestamp,
|
|
32859
|
+
bbox: {
|
|
32860
|
+
x: frame.bbox.x,
|
|
32861
|
+
y: frame.bbox.y,
|
|
32862
|
+
w: frame.bbox.w,
|
|
32863
|
+
h: frame.bbox.h
|
|
32864
|
+
},
|
|
32865
|
+
frameWidth: frame.frameWidth,
|
|
32866
|
+
frameHeight: frame.frameHeight,
|
|
32867
|
+
kind: detail.className === "plate" ? "plate" : "classification",
|
|
32868
|
+
score: detail.score,
|
|
32869
|
+
confidence: frame.confidence,
|
|
32870
|
+
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
32871
|
+
};
|
|
32872
|
+
}
|
|
32873
|
+
/**
|
|
32874
|
+
* Offer the recognition frame to the best-frame ranking with the semantic
|
|
32875
|
+
* tier. On a win: record the best-seen bbox (plausible boxes only — an
|
|
32876
|
+
* envelope-exploded box must not poison the retry reference, mirroring
|
|
32877
|
+
* `buildSnapshotTargets`) and re-fire the captures when the pixels are still
|
|
32878
|
+
* reachable.
|
|
32879
|
+
*/
|
|
32880
|
+
function promoteSemanticBestFrame(deps, input) {
|
|
32881
|
+
const { deviceId, trackId, bbox, frameWidth, frameHeight, timestamp } = input;
|
|
32882
|
+
if (!deps.observeBestFrame({
|
|
32883
|
+
deviceId,
|
|
32884
|
+
trackId,
|
|
32885
|
+
confidence: input.confidence,
|
|
32886
|
+
timestamp,
|
|
32887
|
+
edgeClear: isEdgeClear({
|
|
32888
|
+
bbox,
|
|
32889
|
+
frameWidth,
|
|
32890
|
+
frameHeight
|
|
32891
|
+
}),
|
|
32892
|
+
centerScore: bboxCenterScore(bbox, frameWidth, frameHeight),
|
|
32893
|
+
containment: bboxContainment(bbox, frameWidth, frameHeight),
|
|
32894
|
+
sizeScore: bboxSizeScore(bbox, frameWidth, frameHeight),
|
|
32895
|
+
squareFraction: centralSquareContainmentFraction(bbox, {
|
|
32896
|
+
W: frameWidth,
|
|
32897
|
+
H: frameHeight
|
|
32898
|
+
}),
|
|
32899
|
+
semantic: true
|
|
32900
|
+
})) {
|
|
32901
|
+
deps.logger.debug("best-shot: semantic frame did not beat the held peak", {
|
|
32902
|
+
tags: { deviceId },
|
|
32903
|
+
meta: {
|
|
32904
|
+
trackId,
|
|
32905
|
+
kind: input.kind,
|
|
32906
|
+
score: input.score,
|
|
32907
|
+
timestamp
|
|
32908
|
+
}
|
|
32909
|
+
});
|
|
32910
|
+
return {
|
|
32911
|
+
promoted: false,
|
|
32912
|
+
captured: false
|
|
32913
|
+
};
|
|
32914
|
+
}
|
|
32915
|
+
const plausible = isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, input.confidence);
|
|
32916
|
+
if (plausible) deps.recordBestSeenBbox(deviceId, trackId, {
|
|
32917
|
+
x: bbox.x,
|
|
32918
|
+
y: bbox.y,
|
|
32919
|
+
w: bbox.w,
|
|
32920
|
+
h: bbox.h,
|
|
32921
|
+
frameWidth,
|
|
32922
|
+
frameHeight,
|
|
32923
|
+
timestamp
|
|
32924
|
+
});
|
|
32925
|
+
if (!plausible || input.frameHandle === void 0) {
|
|
32926
|
+
deps.logger.info("best-shot: semantic promotion without capture", {
|
|
32927
|
+
tags: { deviceId },
|
|
32928
|
+
meta: {
|
|
32929
|
+
trackId,
|
|
32930
|
+
kind: input.kind,
|
|
32931
|
+
score: input.score,
|
|
32932
|
+
timestamp,
|
|
32933
|
+
reason: plausible ? "no frame handle" : "implausible box"
|
|
32934
|
+
}
|
|
32935
|
+
});
|
|
32936
|
+
return {
|
|
32937
|
+
promoted: true,
|
|
32938
|
+
captured: false
|
|
32939
|
+
};
|
|
32940
|
+
}
|
|
32941
|
+
deps.captureBestShot({
|
|
32942
|
+
deviceId,
|
|
32943
|
+
trackId,
|
|
32944
|
+
timestamp,
|
|
32945
|
+
bbox,
|
|
32946
|
+
frameWidth,
|
|
32947
|
+
frameHeight,
|
|
32948
|
+
frameHandle: input.frameHandle
|
|
32949
|
+
});
|
|
32950
|
+
deps.logger.info("best-shot: semantic promotion", {
|
|
32951
|
+
tags: { deviceId },
|
|
32952
|
+
meta: {
|
|
32953
|
+
trackId,
|
|
32954
|
+
kind: input.kind,
|
|
32955
|
+
score: input.score,
|
|
32956
|
+
timestamp
|
|
32957
|
+
}
|
|
32958
|
+
});
|
|
32959
|
+
return {
|
|
32960
|
+
promoted: true,
|
|
32961
|
+
captured: true
|
|
32962
|
+
};
|
|
32963
|
+
}
|
|
32763
32964
|
//#endregion
|
|
32764
32965
|
//#region src/pipeline-analytics/store/zone-geometry.ts
|
|
32765
32966
|
/**
|
|
@@ -34498,6 +34699,7 @@ var PlateGalleryProvider = class {
|
|
|
34498
34699
|
refreshGallery;
|
|
34499
34700
|
emitPlateGalleryChanged;
|
|
34500
34701
|
eventMediaBaseUrl;
|
|
34702
|
+
eventMediaPathPrefix;
|
|
34501
34703
|
constructor(deps) {
|
|
34502
34704
|
this.plateStore = deps.plateStore;
|
|
34503
34705
|
this.vehicleStore = deps.vehicleStore;
|
|
@@ -34508,6 +34710,7 @@ var PlateGalleryProvider = class {
|
|
|
34508
34710
|
this.refreshGallery = deps.refreshGallery;
|
|
34509
34711
|
this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
|
|
34510
34712
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
34713
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
34511
34714
|
}
|
|
34512
34715
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
34513
34716
|
safeEmitPlateGalleryChanged(payload) {
|
|
@@ -34578,13 +34781,7 @@ var PlateGalleryProvider = class {
|
|
|
34578
34781
|
return this.toPlateInfo(plate, nameMap, { includeCrops: false });
|
|
34579
34782
|
}
|
|
34580
34783
|
async getPlateMedia(input) {
|
|
34581
|
-
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => (
|
|
34582
|
-
key: m.key,
|
|
34583
|
-
kind: m.kind,
|
|
34584
|
-
base64: m.base64,
|
|
34585
|
-
sizeBytes: m.sizeBytes,
|
|
34586
|
-
timestamp: m.timestamp
|
|
34587
|
-
}));
|
|
34784
|
+
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
34588
34785
|
}
|
|
34589
34786
|
async searchPlates(input) {
|
|
34590
34787
|
const maxDistance = input.maxDistance ?? 1;
|
|
@@ -34827,6 +35024,51 @@ var PlateGalleryProvider = class {
|
|
|
34827
35024
|
}
|
|
34828
35025
|
};
|
|
34829
35026
|
//#endregion
|
|
35027
|
+
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
35028
|
+
/**
|
|
35029
|
+
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
35030
|
+
* return the tracks it produced.
|
|
35031
|
+
*
|
|
35032
|
+
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
35033
|
+
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
35034
|
+
*/
|
|
35035
|
+
function runFrameProcessorPass(input) {
|
|
35036
|
+
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
35037
|
+
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
35038
|
+
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
35039
|
+
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
35040
|
+
const accum = /* @__PURE__ */ new Map();
|
|
35041
|
+
for (const { timestamp, frame } of input.frames) {
|
|
35042
|
+
const result = processor.process({
|
|
35043
|
+
timestamp,
|
|
35044
|
+
frame
|
|
35045
|
+
});
|
|
35046
|
+
for (const td of result.tracked) {
|
|
35047
|
+
const existing = accum.get(td.trackId);
|
|
35048
|
+
const matched = td.matchedThisFrame ? 1 : 0;
|
|
35049
|
+
if (existing === void 0) accum.set(td.trackId, {
|
|
35050
|
+
className: td.className,
|
|
35051
|
+
firstSeenMs: timestamp,
|
|
35052
|
+
lastSeenMs: timestamp,
|
|
35053
|
+
bbox: { ...td.bbox },
|
|
35054
|
+
framesMatched: matched
|
|
35055
|
+
});
|
|
35056
|
+
else {
|
|
35057
|
+
existing.className = td.className;
|
|
35058
|
+
existing.lastSeenMs = timestamp;
|
|
35059
|
+
existing.framesMatched += matched;
|
|
35060
|
+
}
|
|
35061
|
+
}
|
|
35062
|
+
}
|
|
35063
|
+
return { tracks: [...accum.values()].map((t) => ({
|
|
35064
|
+
className: t.className,
|
|
35065
|
+
firstSeenMs: t.firstSeenMs,
|
|
35066
|
+
lastSeenMs: t.lastSeenMs,
|
|
35067
|
+
bbox: t.bbox,
|
|
35068
|
+
framesMatched: t.framesMatched
|
|
35069
|
+
})) };
|
|
35070
|
+
}
|
|
35071
|
+
//#endregion
|
|
34830
35072
|
//#region src/pipeline-analytics/retention/disk-reconcile.ts
|
|
34831
35073
|
/**
|
|
34832
35074
|
* Disk-wins reconcile for pipeline analytics.
|
|
@@ -34891,93 +35133,6 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
34891
35133
|
};
|
|
34892
35134
|
}
|
|
34893
35135
|
//#endregion
|
|
34894
|
-
//#region src/pipeline-analytics/retention/media-reclaim-actions.ts
|
|
34895
|
-
/**
|
|
34896
|
-
* The operator button for the debug-media reclaim.
|
|
34897
|
-
*
|
|
34898
|
-
* The mechanism ships armed with nothing: **arming it is the operator's
|
|
34899
|
-
* call**. `mode` omitted means `report` — it counts, logs one line per camera
|
|
34900
|
-
* and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
|
|
34901
|
-
* that has to be typed.
|
|
34902
|
-
*
|
|
34903
|
-
* Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
|
|
34904
|
-
* earned it: dry-run default, explicit pacing, explicit blast radius, and a
|
|
34905
|
-
* report you can read instead of a log line you have to find.
|
|
34906
|
-
*/
|
|
34907
|
-
/**
|
|
34908
|
-
* `report` counts; `reclaim` deletes.
|
|
34909
|
-
*
|
|
34910
|
-
* `.default('report')` is load-bearing: the settings form sends `{}` for a
|
|
34911
|
-
* button with no fields, and the safe reading of an unstated intent is the one
|
|
34912
|
-
* that destroys nothing.
|
|
34913
|
-
*/
|
|
34914
|
-
var MediaReclaimActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
34915
|
-
var MediaReclaimScopeSchema = require_dist._enum(["motion-still", "track-filmstrip"]);
|
|
34916
|
-
var MediaReclaimActionInputSchema = require_dist.object({
|
|
34917
|
-
mode: MediaReclaimActionModeSchema,
|
|
34918
|
-
/** Which populations to walk. Omitted = both. */
|
|
34919
|
-
scopes: require_dist.array(MediaReclaimScopeSchema).min(1).optional(),
|
|
34920
|
-
/** Restrict to these cameras. Omitted = every camera in retention scope. */
|
|
34921
|
-
deviceIds: require_dist.array(require_dist.number().int()).min(1).optional(),
|
|
34922
|
-
/** Start every device from the top instead of resuming its cursor. */
|
|
34923
|
-
restart: require_dist.boolean().optional(),
|
|
34924
|
-
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
34925
|
-
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
34926
|
-
/** Rows EXAMINED per camera in this pass — the pacing bound. */
|
|
34927
|
-
maxRowsPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
34928
|
-
/** Rows DELETED per camera in this pass — the blast-radius bound. */
|
|
34929
|
-
maxReclaimPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
34930
|
-
/** Bytes released across the whole run — the disk-contention bound. */
|
|
34931
|
-
maxBytesPerRun: require_dist.number().int().min(1).optional(),
|
|
34932
|
-
/** Wall-clock ceiling for the run. */
|
|
34933
|
-
budgetMinutes: require_dist.number().int().min(1).max(720).optional(),
|
|
34934
|
-
/**
|
|
34935
|
-
* Bytes per second the pass may release. The recordings volume sustains
|
|
34936
|
-
* ~4 MB/s and the archive needs it more than this does; the default is half
|
|
34937
|
-
* of that.
|
|
34938
|
-
*/
|
|
34939
|
-
throttleBytesPerSec: require_dist.number().int().min(64 * 1024).optional(),
|
|
34940
|
-
/**
|
|
34941
|
-
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
34942
|
-
* exclusively save rows, never select them.
|
|
34943
|
-
*/
|
|
34944
|
-
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
34945
|
-
});
|
|
34946
|
-
var MediaReclaimDeviceReportSchema = require_dist.object({
|
|
34947
|
-
deviceId: require_dist.number(),
|
|
34948
|
-
scope: MediaReclaimScopeSchema,
|
|
34949
|
-
examined: require_dist.number(),
|
|
34950
|
-
eligible: require_dist.number(),
|
|
34951
|
-
reclaimed: require_dist.number(),
|
|
34952
|
-
bytesReclaimed: require_dist.number(),
|
|
34953
|
-
/** Rows kept because the owning track is pinned (`staging`/`favourited`). */
|
|
34954
|
-
held: require_dist.number(),
|
|
34955
|
-
/** Rows kept because the owner could not be shown to be a motion event. */
|
|
34956
|
-
unresolved: require_dist.number(),
|
|
34957
|
-
withinGrace: require_dist.number(),
|
|
34958
|
-
/** Deletes that REFUSED — the blob survived or the volume could not answer.
|
|
34959
|
-
* The row was kept and the cursor did not pass it. */
|
|
34960
|
-
refused: require_dist.number(),
|
|
34961
|
-
stoppedBy: require_dist.string(),
|
|
34962
|
-
/** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
|
|
34963
|
-
nextCursor: require_dist.number()
|
|
34964
|
-
});
|
|
34965
|
-
var MediaReclaimActionResultSchema = require_dist.object({
|
|
34966
|
-
/** `false` when a pass was already in flight — nothing was done. */
|
|
34967
|
-
ran: require_dist.boolean(),
|
|
34968
|
-
mode: require_dist._enum(["report", "reclaim"]),
|
|
34969
|
-
devices: require_dist.array(MediaReclaimDeviceReportSchema),
|
|
34970
|
-
totalExamined: require_dist.number(),
|
|
34971
|
-
totalEligible: require_dist.number(),
|
|
34972
|
-
totalReclaimed: require_dist.number(),
|
|
34973
|
-
totalBytesReclaimed: require_dist.number(),
|
|
34974
|
-
totalRefused: require_dist.number(),
|
|
34975
|
-
durationMs: require_dist.number(),
|
|
34976
|
-
/** Every device+scope reported `exhausted` — there is nothing left to do. */
|
|
34977
|
-
complete: require_dist.boolean()
|
|
34978
|
-
});
|
|
34979
|
-
var mediaReclaimActions = require_dist.defineCustomActions({ "retention.mediaReclaim": require_dist.customAction(MediaReclaimActionInputSchema, MediaReclaimActionResultSchema, { auth: "admin" }) });
|
|
34980
|
-
//#endregion
|
|
34981
35136
|
//#region src/pipeline-analytics/retention/media-reclaim.ts
|
|
34982
35137
|
var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
|
|
34983
35138
|
var DEFAULT_MEDIA_RECLAIM_LIMITS = {
|
|
@@ -35189,6 +35344,93 @@ async function runMediaReclaim(input) {
|
|
|
35189
35344
|
};
|
|
35190
35345
|
}
|
|
35191
35346
|
//#endregion
|
|
35347
|
+
//#region src/pipeline-analytics/retention/media-reclaim-actions.ts
|
|
35348
|
+
/**
|
|
35349
|
+
* The operator button for the debug-media reclaim.
|
|
35350
|
+
*
|
|
35351
|
+
* The mechanism ships armed with nothing: **arming it is the operator's
|
|
35352
|
+
* call**. `mode` omitted means `report` — it counts, logs one line per camera
|
|
35353
|
+
* and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
|
|
35354
|
+
* that has to be typed.
|
|
35355
|
+
*
|
|
35356
|
+
* Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
|
|
35357
|
+
* earned it: dry-run default, explicit pacing, explicit blast radius, and a
|
|
35358
|
+
* report you can read instead of a log line you have to find.
|
|
35359
|
+
*/
|
|
35360
|
+
/**
|
|
35361
|
+
* `report` counts; `reclaim` deletes.
|
|
35362
|
+
*
|
|
35363
|
+
* `.default('report')` is load-bearing: the settings form sends `{}` for a
|
|
35364
|
+
* button with no fields, and the safe reading of an unstated intent is the one
|
|
35365
|
+
* that destroys nothing.
|
|
35366
|
+
*/
|
|
35367
|
+
var MediaReclaimActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
35368
|
+
var MediaReclaimScopeSchema = require_dist._enum(["motion-still", "track-filmstrip"]);
|
|
35369
|
+
var MediaReclaimActionInputSchema = require_dist.object({
|
|
35370
|
+
mode: MediaReclaimActionModeSchema,
|
|
35371
|
+
/** Which populations to walk. Omitted = both. */
|
|
35372
|
+
scopes: require_dist.array(MediaReclaimScopeSchema).min(1).optional(),
|
|
35373
|
+
/** Restrict to these cameras. Omitted = every camera in retention scope. */
|
|
35374
|
+
deviceIds: require_dist.array(require_dist.number().int()).min(1).optional(),
|
|
35375
|
+
/** Start every device from the top instead of resuming its cursor. */
|
|
35376
|
+
restart: require_dist.boolean().optional(),
|
|
35377
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
35378
|
+
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
35379
|
+
/** Rows EXAMINED per camera in this pass — the pacing bound. */
|
|
35380
|
+
maxRowsPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
35381
|
+
/** Rows DELETED per camera in this pass — the blast-radius bound. */
|
|
35382
|
+
maxReclaimPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
|
|
35383
|
+
/** Bytes released across the whole run — the disk-contention bound. */
|
|
35384
|
+
maxBytesPerRun: require_dist.number().int().min(1).optional(),
|
|
35385
|
+
/** Wall-clock ceiling for the run. */
|
|
35386
|
+
budgetMinutes: require_dist.number().int().min(1).max(720).optional(),
|
|
35387
|
+
/**
|
|
35388
|
+
* Bytes per second the pass may release. The recordings volume sustains
|
|
35389
|
+
* ~4 MB/s and the archive needs it more than this does; the default is half
|
|
35390
|
+
* of that.
|
|
35391
|
+
*/
|
|
35392
|
+
throttleBytesPerSec: require_dist.number().int().min(64 * 1024).optional(),
|
|
35393
|
+
/**
|
|
35394
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
35395
|
+
* exclusively save rows, never select them.
|
|
35396
|
+
*/
|
|
35397
|
+
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
35398
|
+
});
|
|
35399
|
+
var MediaReclaimDeviceReportSchema = require_dist.object({
|
|
35400
|
+
deviceId: require_dist.number(),
|
|
35401
|
+
scope: MediaReclaimScopeSchema,
|
|
35402
|
+
examined: require_dist.number(),
|
|
35403
|
+
eligible: require_dist.number(),
|
|
35404
|
+
reclaimed: require_dist.number(),
|
|
35405
|
+
bytesReclaimed: require_dist.number(),
|
|
35406
|
+
/** Rows kept because the owning track is pinned (`staging`/`favourited`). */
|
|
35407
|
+
held: require_dist.number(),
|
|
35408
|
+
/** Rows kept because the owner could not be shown to be a motion event. */
|
|
35409
|
+
unresolved: require_dist.number(),
|
|
35410
|
+
withinGrace: require_dist.number(),
|
|
35411
|
+
/** Deletes that REFUSED — the blob survived or the volume could not answer.
|
|
35412
|
+
* The row was kept and the cursor did not pass it. */
|
|
35413
|
+
refused: require_dist.number(),
|
|
35414
|
+
stoppedBy: require_dist.string(),
|
|
35415
|
+
/** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
|
|
35416
|
+
nextCursor: require_dist.number()
|
|
35417
|
+
});
|
|
35418
|
+
var MediaReclaimActionResultSchema = require_dist.object({
|
|
35419
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
35420
|
+
ran: require_dist.boolean(),
|
|
35421
|
+
mode: require_dist._enum(["report", "reclaim"]),
|
|
35422
|
+
devices: require_dist.array(MediaReclaimDeviceReportSchema),
|
|
35423
|
+
totalExamined: require_dist.number(),
|
|
35424
|
+
totalEligible: require_dist.number(),
|
|
35425
|
+
totalReclaimed: require_dist.number(),
|
|
35426
|
+
totalBytesReclaimed: require_dist.number(),
|
|
35427
|
+
totalRefused: require_dist.number(),
|
|
35428
|
+
durationMs: require_dist.number(),
|
|
35429
|
+
/** Every device+scope reported `exhausted` — there is nothing left to do. */
|
|
35430
|
+
complete: require_dist.boolean()
|
|
35431
|
+
});
|
|
35432
|
+
var mediaReclaimActions = require_dist.defineCustomActions({ "retention.mediaReclaim": require_dist.customAction(MediaReclaimActionInputSchema, MediaReclaimActionResultSchema, { auth: "admin" }) });
|
|
35433
|
+
//#endregion
|
|
35192
35434
|
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
35193
35435
|
/**
|
|
35194
35436
|
* The operator button for the orphan audit.
|
|
@@ -36545,6 +36787,28 @@ function trackMediaScopesForKinds(trackId, kinds) {
|
|
|
36545
36787
|
return kinds.some((kind) => DETAIL_CROP_KINDS.includes(kind)) ? scopes : scopes.filter((scope) => scope.publishCropAs === null);
|
|
36546
36788
|
}
|
|
36547
36789
|
/**
|
|
36790
|
+
* The STORED kinds worth reading from one scope for a `kinds` request, or
|
|
36791
|
+
* `null` when the scope cannot contribute to it at all.
|
|
36792
|
+
*
|
|
36793
|
+
* This is the same vocabulary translation {@link collectTrackMedia} performs on
|
|
36794
|
+
* the way out, moved to the way IN so it can narrow the query instead of the
|
|
36795
|
+
* answer. A caller naming `faceCrop` must ask the face owner for its stored
|
|
36796
|
+
* `crop` — asking it for `faceCrop` matches nothing, and asking it for
|
|
36797
|
+
* everything opens blobs the answer will discard.
|
|
36798
|
+
*
|
|
36799
|
+
* `undefined` means "every kind": the honest translation of an unfiltered
|
|
36800
|
+
* request, and the only safe answer for the open-ended track scope.
|
|
36801
|
+
*
|
|
36802
|
+
* `null` (skip) is only ever returned for a DETAIL owner whose one publishable
|
|
36803
|
+
* kind was not asked for. The track scope is never skipped — its kinds are
|
|
36804
|
+
* open-ended, and dropping it on a guess is how a listing starts lying.
|
|
36805
|
+
*/
|
|
36806
|
+
function storedKindsForScope(scope, kinds) {
|
|
36807
|
+
if (kinds === void 0 || kinds.length === 0) return void 0;
|
|
36808
|
+
if (scope.publishCropAs === null) return kinds;
|
|
36809
|
+
return kinds.includes(scope.publishCropAs) ? [STORED_DETAIL_CROP_KIND] : null;
|
|
36810
|
+
}
|
|
36811
|
+
/**
|
|
36548
36812
|
* Fold every scope's rows into ONE track media listing: detail crops
|
|
36549
36813
|
* relabelled, everything filtered to `kinds`, ordered oldest first.
|
|
36550
36814
|
*
|
|
@@ -36681,8 +36945,10 @@ var AnalyticsQueryFacade = class {
|
|
|
36681
36945
|
return this.withMediaUrl(events);
|
|
36682
36946
|
}
|
|
36683
36947
|
async getEventMedia(input) {
|
|
36684
|
-
const
|
|
36685
|
-
|
|
36948
|
+
const store = this.deps.mediaStore();
|
|
36949
|
+
if (store === null) return [];
|
|
36950
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36951
|
+
return (await store.listByOwner("event", input.eventId, input.deviceId, input.kind === void 0 ? void 0 : [input.kind])).map((row) => toWireMediaFile(row, prefix));
|
|
36686
36952
|
}
|
|
36687
36953
|
/**
|
|
36688
36954
|
* A track's media WITH the bytes, restricted to `kinds` when given.
|
|
@@ -36695,14 +36961,36 @@ var AnalyticsQueryFacade = class {
|
|
|
36695
36961
|
async getTrackMedia(input) {
|
|
36696
36962
|
const store = this.deps.mediaStore();
|
|
36697
36963
|
if (store === null) return [];
|
|
36964
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36698
36965
|
const scopes = trackMediaScopesForKinds(input.trackId, input.kinds);
|
|
36699
|
-
return collectTrackMedia(await Promise.all(scopes.map(async (scope) =>
|
|
36700
|
-
scope,
|
|
36701
|
-
|
|
36702
|
-
|
|
36966
|
+
return collectTrackMedia(await Promise.all(scopes.map(async (scope) => {
|
|
36967
|
+
const stored = storedKindsForScope(scope, input.kinds);
|
|
36968
|
+
if (stored === null) return {
|
|
36969
|
+
scope,
|
|
36970
|
+
rows: []
|
|
36971
|
+
};
|
|
36972
|
+
return {
|
|
36973
|
+
scope,
|
|
36974
|
+
rows: await store.listByOwner(scope.ownerKind, scope.ownerId, input.deviceId, stored)
|
|
36975
|
+
};
|
|
36976
|
+
})), (row, kind) => ({
|
|
36703
36977
|
...row,
|
|
36704
36978
|
kind
|
|
36705
|
-
}), input.kinds);
|
|
36979
|
+
}), input.kinds).map((row) => toWireMediaFile(row, prefix));
|
|
36980
|
+
}
|
|
36981
|
+
/**
|
|
36982
|
+
* An event's media WITHOUT the bytes.
|
|
36983
|
+
*
|
|
36984
|
+
* Index rows only — it never opens a blob. The track detail modal unions
|
|
36985
|
+
* this with {@link listTrackMedia} to build its filmstrip and then fetches
|
|
36986
|
+
* each tile from the `event-media` plane by key, so the bytes this used to
|
|
36987
|
+
* carry were read, base64'd, shipped and discarded.
|
|
36988
|
+
*/
|
|
36989
|
+
async listEventMedia(input) {
|
|
36990
|
+
const store = this.deps.mediaStore();
|
|
36991
|
+
if (store === null) return [];
|
|
36992
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36993
|
+
return (await store.listInfoByOwner("event", input.eventId, input.deviceId)).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36706
36994
|
}
|
|
36707
36995
|
/**
|
|
36708
36996
|
* A track's media WITHOUT the bytes.
|
|
@@ -36720,13 +37008,14 @@ var AnalyticsQueryFacade = class {
|
|
|
36720
37008
|
async listTrackMedia(input) {
|
|
36721
37009
|
const store = this.deps.mediaStore();
|
|
36722
37010
|
if (store === null) return [];
|
|
37011
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36723
37012
|
return collectTrackMedia(await Promise.all(trackMediaOwnerScopes(input.trackId).map(async (scope) => ({
|
|
36724
37013
|
scope,
|
|
36725
37014
|
rows: await store.listInfoByOwner(scope.ownerKind, scope.ownerId, input.deviceId)
|
|
36726
37015
|
}))), (row, kind) => ({
|
|
36727
37016
|
...row,
|
|
36728
37017
|
kind
|
|
36729
|
-
}), void 0);
|
|
37018
|
+
}), void 0).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36730
37019
|
}
|
|
36731
37020
|
async pruneEventsBefore(input) {
|
|
36732
37021
|
const eventStore = this.deps.eventStore();
|
|
@@ -39171,6 +39460,31 @@ function createEventMediaHandler(deps) {
|
|
|
39171
39460
|
}
|
|
39172
39461
|
//#endregion
|
|
39173
39462
|
//#region src/pipeline-analytics/event-thumbnail-resolver.ts
|
|
39463
|
+
/**
|
|
39464
|
+
* Rank `candidates` with `pick`, then open ONE blob — the winner's.
|
|
39465
|
+
*
|
|
39466
|
+
* The ladders decide on `kind` alone, so the bytes are dead weight until the
|
|
39467
|
+
* choice is made. Handing them pre-read rows meant a tile that serves the
|
|
39468
|
+
* 31 KB `thumbnailSmall` first read the whole 3 597 219-byte set, including a
|
|
39469
|
+
* 2.8 MB `keyFrame` no list view has ever rendered.
|
|
39470
|
+
*
|
|
39471
|
+
* A winner whose blob is GONE is dropped and the ladder re-run on the rest.
|
|
39472
|
+
* That fall-through is not an extra: `listByOwner` used to provide it for free
|
|
39473
|
+
* by skipping unreadable rows before the ladder ever saw them, and without it
|
|
39474
|
+
* an index row that outlived its file would 404 a tile that used to render.
|
|
39475
|
+
* Bounded by the candidate count — each attempt removes one row.
|
|
39476
|
+
*/
|
|
39477
|
+
async function readWinningBlob(candidates, pick, readBlob) {
|
|
39478
|
+
let remaining = candidates;
|
|
39479
|
+
while (remaining.length > 0) {
|
|
39480
|
+
const winner = pick(remaining);
|
|
39481
|
+
if (winner === void 0) return null;
|
|
39482
|
+
const bytes = await readBlob(winner.key);
|
|
39483
|
+
if (bytes !== null) return bytes;
|
|
39484
|
+
remaining = remaining.filter((row) => row.key !== winner.key);
|
|
39485
|
+
}
|
|
39486
|
+
return null;
|
|
39487
|
+
}
|
|
39174
39488
|
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
39175
39489
|
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
39176
39490
|
* for new tracks (served only for old stored rows). Undefined when the event
|
|
@@ -39221,15 +39535,24 @@ async function collectCropForcedMedia(source, id) {
|
|
|
39221
39535
|
};
|
|
39222
39536
|
}
|
|
39223
39537
|
/**
|
|
39224
|
-
*
|
|
39225
|
-
* has any, else the owning track's
|
|
39226
|
-
*
|
|
39538
|
+
* Which rows the default path should serve from, and how to rank them: the
|
|
39539
|
+
* event's OWN media when it has any, else the owning track's.
|
|
39540
|
+
*
|
|
39541
|
+
* It yields the candidate SET rather than a winner so the caller can open one
|
|
39542
|
+
* blob and, if that blob has gone, re-rank the rest ({@link readWinningBlob}).
|
|
39543
|
+
* Returning a single row would force the caller to read it blind and 404 a tile
|
|
39544
|
+
* whose index row outlived its file.
|
|
39227
39545
|
*/
|
|
39228
|
-
async function
|
|
39229
|
-
|
|
39230
|
-
|
|
39546
|
+
async function collectDefaultEventMedia(deps) {
|
|
39547
|
+
if (deps.eventFiles.length > 0) return {
|
|
39548
|
+
candidates: deps.eventFiles,
|
|
39549
|
+
pick: pickEventOwnedMedia
|
|
39550
|
+
};
|
|
39231
39551
|
const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
|
|
39232
|
-
return
|
|
39552
|
+
return {
|
|
39553
|
+
candidates: await deps.listTrackMedia(trackId),
|
|
39554
|
+
pick: pickTrackFallbackMedia
|
|
39555
|
+
};
|
|
39233
39556
|
}
|
|
39234
39557
|
//#endregion
|
|
39235
39558
|
//#region src/pipeline-analytics/face-settings.ts
|
|
@@ -41130,13 +41453,29 @@ var RETRAIN_FRAME_COLUMNS = [
|
|
|
41130
41453
|
type: "TEXT"
|
|
41131
41454
|
})
|
|
41132
41455
|
];
|
|
41133
|
-
var RETRAIN_FRAME_INDEXES = [
|
|
41134
|
-
|
|
41135
|
-
|
|
41136
|
-
|
|
41137
|
-
|
|
41138
|
-
|
|
41139
|
-
|
|
41456
|
+
var RETRAIN_FRAME_INDEXES = [
|
|
41457
|
+
{
|
|
41458
|
+
name: "idx_retrain_frames_track",
|
|
41459
|
+
columns: ["trackId"]
|
|
41460
|
+
},
|
|
41461
|
+
{
|
|
41462
|
+
name: "idx_retrain_frames_device",
|
|
41463
|
+
columns: ["deviceId", "copiedAt"]
|
|
41464
|
+
},
|
|
41465
|
+
(
|
|
41466
|
+
/**
|
|
41467
|
+
* The seal gate's other half. This collection is orders of magnitude smaller
|
|
41468
|
+
* than `pipeline-analytics:media` — every row is an operator-staged frame —
|
|
41469
|
+
* so the index buys much less here. It exists anyway because the gate reads
|
|
41470
|
+
* the two lanes with ONE query shape, and a lane that answers by scanning is
|
|
41471
|
+
* a lane that starts timing out the day the dataset grows. See
|
|
41472
|
+
* `MEDIA_INDEXES` for the cost this is a small multiple of.
|
|
41473
|
+
*/
|
|
41474
|
+
{
|
|
41475
|
+
name: "idx_retrain_frames_location",
|
|
41476
|
+
columns: ["locationId"]
|
|
41477
|
+
})
|
|
41478
|
+
];
|
|
41140
41479
|
/**
|
|
41141
41480
|
* The copy's id, derived from `(trackId, sourceMediaKey)`.
|
|
41142
41481
|
*
|
|
@@ -44402,14 +44741,27 @@ function snapshot(j) {
|
|
|
44402
44741
|
};
|
|
44403
44742
|
}
|
|
44404
44743
|
/**
|
|
44744
|
+
* The column both walked collections use for the storage location.
|
|
44745
|
+
*
|
|
44746
|
+
* Named once because it is now a QUERY PREDICATE (`where: { [it]: null }`) as
|
|
44747
|
+
* well as a row field. A predicate naming a column the collection cannot
|
|
44748
|
+
* express is refused by `count` rather than dropped, so a rename that misses
|
|
44749
|
+
* one site fails loudly instead of counting the whole table.
|
|
44750
|
+
*/
|
|
44751
|
+
var LOCATION_ID_FIELD = "locationId";
|
|
44752
|
+
/**
|
|
44405
44753
|
* True when the row names NO location — the population `seal` exists for.
|
|
44406
44754
|
*
|
|
44407
44755
|
* Deliberately NOT `mediaRowLocation(data) === DEFAULT_MEDIA_LOCATION`: that
|
|
44408
44756
|
* helper collapses "unstamped" and "stamped with the bare type ref" into one
|
|
44409
44757
|
* answer, and only the first is a row whose meaning changes under a repoint.
|
|
44758
|
+
*
|
|
44759
|
+
* The in-JS twin of the `IS NULL` predicate {@link MediaRelocateEngine.countUnstamped}
|
|
44760
|
+
* sends. It stays because the PASS still walks rows and must decide per row;
|
|
44761
|
+
* the two must agree, and `unstamped-count-matches-the-pass` asserts they do.
|
|
44410
44762
|
*/
|
|
44411
44763
|
function rowIsUnstamped(data) {
|
|
44412
|
-
const id = data[
|
|
44764
|
+
const id = data[LOCATION_ID_FIELD];
|
|
44413
44765
|
return !(typeof id === "string" && id.length > 0);
|
|
44414
44766
|
}
|
|
44415
44767
|
/** True for the enrolled-gallery rows `move` skips and `gallery` selects. */
|
|
@@ -44467,26 +44819,91 @@ var MediaRelocateEngine = class {
|
|
|
44467
44819
|
return true;
|
|
44468
44820
|
}
|
|
44469
44821
|
/**
|
|
44470
|
-
*
|
|
44822
|
+
* Rows that still carry no `locationId`, per collection.
|
|
44471
44823
|
*
|
|
44472
|
-
* Read-only, and the ONLY
|
|
44473
|
-
* is allowed to trust: it is
|
|
44824
|
+
* Read-only, and the ONLY answer the migration planner's non-blocking gate
|
|
44825
|
+
* is allowed to trust: it is measured here, against the rows themselves,
|
|
44474
44826
|
* rather than inferred from "a seal job reported done" — a seal that failed
|
|
44475
44827
|
* some rows still reports a terminal state.
|
|
44828
|
+
*
|
|
44829
|
+
* ── Why this is not the walk the rest of this file uses ──────────────────
|
|
44830
|
+
*
|
|
44831
|
+
* It used to be. It paged both collections through {@link pageRows} — 200
|
|
44832
|
+
* rows per RPC, ordered by `timestamp`, a column that leads no index — and
|
|
44833
|
+
* counted `rowIsUnstamped` in JS. On the live hub that is ~6 300 round trips
|
|
44834
|
+
* over 1 254 576 rows, each paying its own sort, and it hit the 60 s RPC
|
|
44835
|
+
* deadline **every time it was called since it shipped**. The gate it exists
|
|
44836
|
+
* to serve could therefore never open, and 152 GB of event media could not
|
|
44837
|
+
* be migrated.
|
|
44838
|
+
*
|
|
44839
|
+
* The walk is right for the PASS, which must visit every row to act on it.
|
|
44840
|
+
* It was never right for a question the store can answer with a predicate,
|
|
44841
|
+
* and the predicate is now expressible: `where: { locationId: null }`
|
|
44842
|
+
* compiles to `IS NULL` (it used to compile to `= NULL`, which matches
|
|
44843
|
+
* nothing and reports it as an answer — the number the gate treats as "safe
|
|
44844
|
+
* to cut over"). With `idx_media_location` in place both statements read a
|
|
44845
|
+
* contiguous range from the head of a b-tree.
|
|
44846
|
+
*
|
|
44847
|
+
* The cheap question is asked FIRST and on its own. `present` is a single
|
|
44848
|
+
* seek and decides the gate; `rows` is a second statement and only decorates
|
|
44849
|
+
* the refusal. So a count that cannot be taken still leaves a usable answer
|
|
44850
|
+
* — `{ present: true, rows: null }` refuses just as hard — and `null` never
|
|
44851
|
+
* degrades into `0`.
|
|
44476
44852
|
*/
|
|
44477
44853
|
async countUnstamped() {
|
|
44478
|
-
|
|
44479
|
-
|
|
44480
|
-
|
|
44481
|
-
|
|
44482
|
-
|
|
44483
|
-
|
|
44484
|
-
|
|
44854
|
+
try {
|
|
44855
|
+
const media = await this.countUnstampedIn(MEDIA_COLLECTION);
|
|
44856
|
+
const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION);
|
|
44857
|
+
return {
|
|
44858
|
+
media,
|
|
44859
|
+
retrainFrames,
|
|
44860
|
+
anyPresent: media.present || retrainFrames.present,
|
|
44861
|
+
total: media.rows === null || retrainFrames.rows === null ? null : media.rows + retrainFrames.rows
|
|
44862
|
+
};
|
|
44863
|
+
} catch (err) {
|
|
44864
|
+
this.deps.logger.warn("event-media seal gate: could not determine whether any row is unstamped — a migration must treat this as unknown, never as sealed", { meta: { error: String(err) } });
|
|
44865
|
+
return null;
|
|
44866
|
+
}
|
|
44485
44867
|
}
|
|
44486
|
-
|
|
44487
|
-
|
|
44488
|
-
|
|
44489
|
-
|
|
44868
|
+
/**
|
|
44869
|
+
* One collection's unstamped population: existence, then size.
|
|
44870
|
+
*
|
|
44871
|
+
* The `limit: 1` probe is deliberately a `query` and not a `count`: a count
|
|
44872
|
+
* of a million matching rows still walks a million index entries, while the
|
|
44873
|
+
* probe stops at the first one. Absence is proved by the same seek finding
|
|
44874
|
+
* nothing, which is where the index earns its keep — after a seal, the range
|
|
44875
|
+
* is empty and the answer is one b-tree descent.
|
|
44876
|
+
*/
|
|
44877
|
+
async countUnstampedIn(collection) {
|
|
44878
|
+
if ((await this.deps.store.query.query({
|
|
44879
|
+
collection,
|
|
44880
|
+
filter: {
|
|
44881
|
+
where: { [LOCATION_ID_FIELD]: null },
|
|
44882
|
+
limit: 1
|
|
44883
|
+
},
|
|
44884
|
+
columns: [LOCATION_ID_FIELD]
|
|
44885
|
+
})).length === 0) return {
|
|
44886
|
+
present: false,
|
|
44887
|
+
rows: 0
|
|
44888
|
+
};
|
|
44889
|
+
try {
|
|
44890
|
+
return {
|
|
44891
|
+
present: true,
|
|
44892
|
+
rows: await this.deps.store.count.query({
|
|
44893
|
+
collection,
|
|
44894
|
+
filter: { where: { [LOCATION_ID_FIELD]: null } }
|
|
44895
|
+
})
|
|
44896
|
+
};
|
|
44897
|
+
} catch (err) {
|
|
44898
|
+
this.deps.logger.warn("event-media seal gate: unstamped rows exist but could not be counted — reporting the population as unknown, not as empty", { meta: {
|
|
44899
|
+
collection,
|
|
44900
|
+
error: String(err)
|
|
44901
|
+
} });
|
|
44902
|
+
return {
|
|
44903
|
+
present: true,
|
|
44904
|
+
rows: null
|
|
44905
|
+
};
|
|
44906
|
+
}
|
|
44490
44907
|
}
|
|
44491
44908
|
start(input) {
|
|
44492
44909
|
for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
|
|
@@ -47243,6 +47660,70 @@ var PackageDropDetector = class {
|
|
|
47243
47660
|
}
|
|
47244
47661
|
};
|
|
47245
47662
|
//#endregion
|
|
47663
|
+
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
47664
|
+
/**
|
|
47665
|
+
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
47666
|
+
*
|
|
47667
|
+
* 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
|
|
47668
|
+
* routinely pokes a little outside the vehicle box, and requiring total
|
|
47669
|
+
* containment would count almost none of them. 0.8 and not 0.5: half a person
|
|
47670
|
+
* box overlapping a car is what a pedestrian walking in front of one looks
|
|
47671
|
+
* like, and counting those would make the number mean "somebody was near a
|
|
47672
|
+
* car", which is not a defect.
|
|
47673
|
+
*/
|
|
47674
|
+
var CONTAINMENT_THRESHOLD = .8;
|
|
47675
|
+
/**
|
|
47676
|
+
* The classes that count as a vehicle for this test.
|
|
47677
|
+
*
|
|
47678
|
+
* A local list rather than a shared taxonomy on purpose: it exists to describe
|
|
47679
|
+
* ONE measurement, and a shared enum would make a change here silently change
|
|
47680
|
+
* detection behaviour somewhere else.
|
|
47681
|
+
*/
|
|
47682
|
+
var VEHICLE_CLASSES$1 = [
|
|
47683
|
+
"car",
|
|
47684
|
+
"truck",
|
|
47685
|
+
"bus",
|
|
47686
|
+
"motorcycle",
|
|
47687
|
+
"vehicle"
|
|
47688
|
+
];
|
|
47689
|
+
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
47690
|
+
function intersectionArea(a, b) {
|
|
47691
|
+
const left = Math.max(a.x, b.x);
|
|
47692
|
+
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
47693
|
+
const top = Math.max(a.y, b.y);
|
|
47694
|
+
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
47695
|
+
if (right <= left || bottom <= top) return 0;
|
|
47696
|
+
return (right - left) * (bottom - top);
|
|
47697
|
+
}
|
|
47698
|
+
/**
|
|
47699
|
+
* Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
|
|
47700
|
+
*
|
|
47701
|
+
* `0` for a degenerate person box: a zero-area box is inside everything and
|
|
47702
|
+
* inside nothing, and answering `1` would count every one of them.
|
|
47703
|
+
*/
|
|
47704
|
+
function containedFraction(person, vehicle) {
|
|
47705
|
+
const area = person.w * person.h;
|
|
47706
|
+
if (area <= 0) return 0;
|
|
47707
|
+
return intersectionArea(person, vehicle) / area;
|
|
47708
|
+
}
|
|
47709
|
+
/**
|
|
47710
|
+
* Is this person box contained in any vehicle box in the same frame?
|
|
47711
|
+
*
|
|
47712
|
+
* Returns the fraction for the BEST-containing vehicle when it clears
|
|
47713
|
+
* {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
|
|
47714
|
+
* a counter that only ever says "yes" cannot later be re-thresholded against
|
|
47715
|
+
* what was actually measured.
|
|
47716
|
+
*/
|
|
47717
|
+
function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
|
|
47718
|
+
let best = 0;
|
|
47719
|
+
for (const neighbour of neighbours) {
|
|
47720
|
+
if (!VEHICLE_CLASSES$1.includes(neighbour.className)) continue;
|
|
47721
|
+
const fraction = containedFraction(person, neighbour.bbox);
|
|
47722
|
+
if (fraction > best) best = fraction;
|
|
47723
|
+
}
|
|
47724
|
+
return best >= threshold ? best : null;
|
|
47725
|
+
}
|
|
47726
|
+
//#endregion
|
|
47246
47727
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
|
|
47247
47728
|
/**
|
|
47248
47729
|
* stationary-evidence — how a parked object EARNS a registry entry.
|
|
@@ -49774,83 +50255,6 @@ function buildRecordedStillPorts(input) {
|
|
|
49774
50255
|
})
|
|
49775
50256
|
};
|
|
49776
50257
|
}
|
|
49777
|
-
function errorMessage(err) {
|
|
49778
|
-
return err instanceof Error ? err.message : String(err);
|
|
49779
|
-
}
|
|
49780
|
-
async function reconcileGallerySamples(deps, options) {
|
|
49781
|
-
let checked = 0;
|
|
49782
|
-
let reclaimed = 0;
|
|
49783
|
-
let keyless = 0;
|
|
49784
|
-
let skippedGrace = 0;
|
|
49785
|
-
let stoppedByCeiling = false;
|
|
49786
|
-
const identities = await deps.listIdentities();
|
|
49787
|
-
for (const identity of identities) {
|
|
49788
|
-
const samples = await deps.listSamples(identity.id);
|
|
49789
|
-
for (const sample of samples) {
|
|
49790
|
-
if (sample.mediaKey === void 0) {
|
|
49791
|
-
keyless += 1;
|
|
49792
|
-
continue;
|
|
49793
|
-
}
|
|
49794
|
-
if (options.now - sample.addedAt < options.graceMs) {
|
|
49795
|
-
skippedGrace += 1;
|
|
49796
|
-
continue;
|
|
49797
|
-
}
|
|
49798
|
-
if (reclaimed >= options.maxReclaimPerRun) {
|
|
49799
|
-
stoppedByCeiling = true;
|
|
49800
|
-
deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
|
|
49801
|
-
return {
|
|
49802
|
-
checked,
|
|
49803
|
-
reclaimed,
|
|
49804
|
-
keyless,
|
|
49805
|
-
skippedGrace,
|
|
49806
|
-
stoppedByCeiling
|
|
49807
|
-
};
|
|
49808
|
-
}
|
|
49809
|
-
let state;
|
|
49810
|
-
try {
|
|
49811
|
-
state = await deps.blobState(sample.mediaKey);
|
|
49812
|
-
} catch (err) {
|
|
49813
|
-
const reason = errorMessage(err);
|
|
49814
|
-
deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
|
|
49815
|
-
sampleId: sample.id,
|
|
49816
|
-
identity: identity.name,
|
|
49817
|
-
error: reason
|
|
49818
|
-
} });
|
|
49819
|
-
return {
|
|
49820
|
-
checked,
|
|
49821
|
-
reclaimed,
|
|
49822
|
-
keyless,
|
|
49823
|
-
skippedGrace,
|
|
49824
|
-
stoppedByCeiling,
|
|
49825
|
-
aborted: reason
|
|
49826
|
-
};
|
|
49827
|
-
}
|
|
49828
|
-
checked += 1;
|
|
49829
|
-
if (state === "exists") continue;
|
|
49830
|
-
await deps.removeSample(identity.id, sample.id);
|
|
49831
|
-
reclaimed += 1;
|
|
49832
|
-
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
49833
|
-
sampleId: sample.id,
|
|
49834
|
-
identity: identity.name,
|
|
49835
|
-
mediaKey: sample.mediaKey,
|
|
49836
|
-
state
|
|
49837
|
-
} });
|
|
49838
|
-
}
|
|
49839
|
-
}
|
|
49840
|
-
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
49841
|
-
checked,
|
|
49842
|
-
reclaimed,
|
|
49843
|
-
keyless,
|
|
49844
|
-
skippedGrace
|
|
49845
|
-
} });
|
|
49846
|
-
return {
|
|
49847
|
-
checked,
|
|
49848
|
-
reclaimed,
|
|
49849
|
-
keyless,
|
|
49850
|
-
skippedGrace,
|
|
49851
|
-
stoppedByCeiling
|
|
49852
|
-
};
|
|
49853
|
-
}
|
|
49854
50258
|
//#endregion
|
|
49855
50259
|
//#region src/pipeline-analytics/retention/embedding-orphan-scan.ts
|
|
49856
50260
|
/** The scope id this leg reports and persists its cursor under. */
|
|
@@ -49967,6 +50371,83 @@ async function scanEmbeddingOrphans(deps, limits, cursor) {
|
|
|
49967
50371
|
nextCursor: Math.max(0, offset)
|
|
49968
50372
|
};
|
|
49969
50373
|
}
|
|
50374
|
+
function errorMessage(err) {
|
|
50375
|
+
return err instanceof Error ? err.message : String(err);
|
|
50376
|
+
}
|
|
50377
|
+
async function reconcileGallerySamples(deps, options) {
|
|
50378
|
+
let checked = 0;
|
|
50379
|
+
let reclaimed = 0;
|
|
50380
|
+
let keyless = 0;
|
|
50381
|
+
let skippedGrace = 0;
|
|
50382
|
+
let stoppedByCeiling = false;
|
|
50383
|
+
const identities = await deps.listIdentities();
|
|
50384
|
+
for (const identity of identities) {
|
|
50385
|
+
const samples = await deps.listSamples(identity.id);
|
|
50386
|
+
for (const sample of samples) {
|
|
50387
|
+
if (sample.mediaKey === void 0) {
|
|
50388
|
+
keyless += 1;
|
|
50389
|
+
continue;
|
|
50390
|
+
}
|
|
50391
|
+
if (options.now - sample.addedAt < options.graceMs) {
|
|
50392
|
+
skippedGrace += 1;
|
|
50393
|
+
continue;
|
|
50394
|
+
}
|
|
50395
|
+
if (reclaimed >= options.maxReclaimPerRun) {
|
|
50396
|
+
stoppedByCeiling = true;
|
|
50397
|
+
deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
|
|
50398
|
+
return {
|
|
50399
|
+
checked,
|
|
50400
|
+
reclaimed,
|
|
50401
|
+
keyless,
|
|
50402
|
+
skippedGrace,
|
|
50403
|
+
stoppedByCeiling
|
|
50404
|
+
};
|
|
50405
|
+
}
|
|
50406
|
+
let state;
|
|
50407
|
+
try {
|
|
50408
|
+
state = await deps.blobState(sample.mediaKey);
|
|
50409
|
+
} catch (err) {
|
|
50410
|
+
const reason = errorMessage(err);
|
|
50411
|
+
deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
|
|
50412
|
+
sampleId: sample.id,
|
|
50413
|
+
identity: identity.name,
|
|
50414
|
+
error: reason
|
|
50415
|
+
} });
|
|
50416
|
+
return {
|
|
50417
|
+
checked,
|
|
50418
|
+
reclaimed,
|
|
50419
|
+
keyless,
|
|
50420
|
+
skippedGrace,
|
|
50421
|
+
stoppedByCeiling,
|
|
50422
|
+
aborted: reason
|
|
50423
|
+
};
|
|
50424
|
+
}
|
|
50425
|
+
checked += 1;
|
|
50426
|
+
if (state === "exists") continue;
|
|
50427
|
+
await deps.removeSample(identity.id, sample.id);
|
|
50428
|
+
reclaimed += 1;
|
|
50429
|
+
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
50430
|
+
sampleId: sample.id,
|
|
50431
|
+
identity: identity.name,
|
|
50432
|
+
mediaKey: sample.mediaKey,
|
|
50433
|
+
state
|
|
50434
|
+
} });
|
|
50435
|
+
}
|
|
50436
|
+
}
|
|
50437
|
+
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
50438
|
+
checked,
|
|
50439
|
+
reclaimed,
|
|
50440
|
+
keyless,
|
|
50441
|
+
skippedGrace
|
|
50442
|
+
} });
|
|
50443
|
+
return {
|
|
50444
|
+
checked,
|
|
50445
|
+
reclaimed,
|
|
50446
|
+
keyless,
|
|
50447
|
+
skippedGrace,
|
|
50448
|
+
stoppedByCeiling
|
|
50449
|
+
};
|
|
50450
|
+
}
|
|
49970
50451
|
/**
|
|
49971
50452
|
* Where the completion marker lives.
|
|
49972
50453
|
*
|
|
@@ -53029,14 +53510,14 @@ function isAfterCursor(row, cursor) {
|
|
|
53029
53510
|
if (row.lastSeen < cursor.lastSeen) return true;
|
|
53030
53511
|
return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
|
|
53031
53512
|
}
|
|
53032
|
-
var VEHICLE_CLASSES
|
|
53513
|
+
var VEHICLE_CLASSES = new Set([
|
|
53033
53514
|
"vehicle",
|
|
53034
53515
|
"car",
|
|
53035
53516
|
"truck",
|
|
53036
53517
|
"bus"
|
|
53037
53518
|
]);
|
|
53038
53519
|
function motionlessMaxPxForClass(className) {
|
|
53039
|
-
return VEHICLE_CLASSES
|
|
53520
|
+
return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
|
|
53040
53521
|
}
|
|
53041
53522
|
function netDisplacementPx(positions) {
|
|
53042
53523
|
const first = positions[0];
|
|
@@ -56796,70 +57277,6 @@ function hasWrappedDetection(entries) {
|
|
|
56796
57277
|
return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
|
|
56797
57278
|
}
|
|
56798
57279
|
//#endregion
|
|
56799
|
-
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
56800
|
-
/**
|
|
56801
|
-
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
56802
|
-
*
|
|
56803
|
-
* 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
|
|
56804
|
-
* routinely pokes a little outside the vehicle box, and requiring total
|
|
56805
|
-
* containment would count almost none of them. 0.8 and not 0.5: half a person
|
|
56806
|
-
* box overlapping a car is what a pedestrian walking in front of one looks
|
|
56807
|
-
* like, and counting those would make the number mean "somebody was near a
|
|
56808
|
-
* car", which is not a defect.
|
|
56809
|
-
*/
|
|
56810
|
-
var CONTAINMENT_THRESHOLD = .8;
|
|
56811
|
-
/**
|
|
56812
|
-
* The classes that count as a vehicle for this test.
|
|
56813
|
-
*
|
|
56814
|
-
* A local list rather than a shared taxonomy on purpose: it exists to describe
|
|
56815
|
-
* ONE measurement, and a shared enum would make a change here silently change
|
|
56816
|
-
* detection behaviour somewhere else.
|
|
56817
|
-
*/
|
|
56818
|
-
var VEHICLE_CLASSES = [
|
|
56819
|
-
"car",
|
|
56820
|
-
"truck",
|
|
56821
|
-
"bus",
|
|
56822
|
-
"motorcycle",
|
|
56823
|
-
"vehicle"
|
|
56824
|
-
];
|
|
56825
|
-
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
56826
|
-
function intersectionArea(a, b) {
|
|
56827
|
-
const left = Math.max(a.x, b.x);
|
|
56828
|
-
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
56829
|
-
const top = Math.max(a.y, b.y);
|
|
56830
|
-
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
56831
|
-
if (right <= left || bottom <= top) return 0;
|
|
56832
|
-
return (right - left) * (bottom - top);
|
|
56833
|
-
}
|
|
56834
|
-
/**
|
|
56835
|
-
* Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
|
|
56836
|
-
*
|
|
56837
|
-
* `0` for a degenerate person box: a zero-area box is inside everything and
|
|
56838
|
-
* inside nothing, and answering `1` would count every one of them.
|
|
56839
|
-
*/
|
|
56840
|
-
function containedFraction(person, vehicle) {
|
|
56841
|
-
const area = person.w * person.h;
|
|
56842
|
-
if (area <= 0) return 0;
|
|
56843
|
-
return intersectionArea(person, vehicle) / area;
|
|
56844
|
-
}
|
|
56845
|
-
/**
|
|
56846
|
-
* Is this person box contained in any vehicle box in the same frame?
|
|
56847
|
-
*
|
|
56848
|
-
* Returns the fraction for the BEST-containing vehicle when it clears
|
|
56849
|
-
* {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
|
|
56850
|
-
* a counter that only ever says "yes" cannot later be re-thresholded against
|
|
56851
|
-
* what was actually measured.
|
|
56852
|
-
*/
|
|
56853
|
-
function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
|
|
56854
|
-
let best = 0;
|
|
56855
|
-
for (const neighbour of neighbours) {
|
|
56856
|
-
if (!VEHICLE_CLASSES.includes(neighbour.className)) continue;
|
|
56857
|
-
const fraction = containedFraction(person, neighbour.bbox);
|
|
56858
|
-
if (fraction > best) best = fraction;
|
|
56859
|
-
}
|
|
56860
|
-
return best >= threshold ? best : null;
|
|
56861
|
-
}
|
|
56862
|
-
//#endregion
|
|
56863
57280
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
56864
57281
|
function createCaptureCrop(deps) {
|
|
56865
57282
|
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
@@ -63301,6 +63718,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63301
63718
|
* Set once the data-plane is registered; null until then (e.g. no
|
|
63302
63719
|
* dataPlane facility in the current environment). */
|
|
63303
63720
|
eventMediaBaseUrl = null;
|
|
63721
|
+
/**
|
|
63722
|
+
* The SAME path, derived rather than observed — a pure function of the addon
|
|
63723
|
+
* id, so it exists before `serve()` is attempted and survives its failure.
|
|
63724
|
+
*
|
|
63725
|
+
* `eventMediaBaseUrl` above answers "is the plane up?", which is the right
|
|
63726
|
+
* question for the OPTIONAL `mediaUrl` stamp on a slim event projection.
|
|
63727
|
+
* It is the wrong question for a media row's `url`, which is that row's only
|
|
63728
|
+
* address: a row that dropped its address on a boot race would be
|
|
63729
|
+
* indistinguishable from a row with no bytes, and this repo has already paid
|
|
63730
|
+
* for one read that quietly returned less than it was asked for.
|
|
63731
|
+
*/
|
|
63732
|
+
eventMediaPathPrefix() {
|
|
63733
|
+
return `/addon/${this.ctx.id}/event-media`;
|
|
63734
|
+
}
|
|
63304
63735
|
lastActiveTrackIds = /* @__PURE__ */ new Map();
|
|
63305
63736
|
/** See `pipeline/suppressed-births.ts` — rejected births must not be
|
|
63306
63737
|
* re-upserted, and must be forgotten when the tracker drops the id. */
|
|
@@ -63551,6 +63982,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63551
63982
|
sensorEventStore: () => this.sensorEventStore,
|
|
63552
63983
|
opsLog: () => this.eventsOpsLog,
|
|
63553
63984
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
63985
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
63554
63986
|
logger: () => this.ctx.logger,
|
|
63555
63987
|
zoneCatalogue: () => this.zoneCatalogue
|
|
63556
63988
|
});
|
|
@@ -66093,6 +66525,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66093
66525
|
logger: this.ctx.logger,
|
|
66094
66526
|
refreshGallery: () => this.faceRecognizer?.refreshGallery(),
|
|
66095
66527
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66528
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66096
66529
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload)
|
|
66097
66530
|
});
|
|
66098
66531
|
this.faceGalleryProvider = faceGallery;
|
|
@@ -66107,6 +66540,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66107
66540
|
this.plateRecognizer?.refreshGallery();
|
|
66108
66541
|
},
|
|
66109
66542
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66543
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66110
66544
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
|
|
66111
66545
|
});
|
|
66112
66546
|
return [
|
|
@@ -69792,6 +70226,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
69792
70226
|
async listTrackMedia(input) {
|
|
69793
70227
|
return this.queryFacade.listTrackMedia(input);
|
|
69794
70228
|
}
|
|
70229
|
+
/** What media an EVENT has, without any of it — the same manifest, for the
|
|
70230
|
+
* half of the detail modal's filmstrip that is event-owned. */
|
|
70231
|
+
async listEventMedia(input) {
|
|
70232
|
+
return this.queryFacade.listEventMedia(input);
|
|
70233
|
+
}
|
|
69795
70234
|
/**
|
|
69796
70235
|
* Search object events by text using CLIP cosine similarity.
|
|
69797
70236
|
*
|
|
@@ -71512,31 +71951,44 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71512
71951
|
return media;
|
|
71513
71952
|
}
|
|
71514
71953
|
}
|
|
71954
|
+
/**
|
|
71955
|
+
* Resolve an event/track id to ONE tile's bytes.
|
|
71956
|
+
*
|
|
71957
|
+
* Every ladder below ranks rows by `kind` alone, so this walks the INDEX
|
|
71958
|
+
* (`listInfoByOwner`, no blob opened) and opens only the winner. It used to
|
|
71959
|
+
* walk `listByOwner`, which reads and base64-encodes every blob an owner
|
|
71960
|
+
* holds before the first comparison is made: measured on the live hub
|
|
71961
|
+
* 2026-08-30, a track's six kinds are 3 597 219 bytes and the tile serves
|
|
71962
|
+
* `thumbnailSmall` — 31 155 of them. An Events page of forty tiles read
|
|
71963
|
+
* ~144 MB and allocated ~190 MB of base64 to deliver ~1.2 MB, all of it on
|
|
71964
|
+
* the single-threaded post-analysis runner.
|
|
71965
|
+
*/
|
|
71515
71966
|
async readEventThumbnail(id, preferKind) {
|
|
71516
|
-
const
|
|
71967
|
+
const listInfo = (ownerKind, ownerId) => this.mediaStore?.listInfoByOwner(ownerKind, ownerId) ?? Promise.resolve([]);
|
|
71968
|
+
const getTrackIdForEvent = (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null);
|
|
71969
|
+
const readBlob = async (key) => {
|
|
71970
|
+
const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
|
|
71971
|
+
return file === null ? null : {
|
|
71972
|
+
bytes: Buffer.from(file.base64, "base64"),
|
|
71973
|
+
key: file.key
|
|
71974
|
+
};
|
|
71975
|
+
};
|
|
71976
|
+
const eventFiles = await listInfo("event", id);
|
|
71517
71977
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
71518
71978
|
const layers = await collectCropForcedMedia({
|
|
71519
|
-
listByOwner:
|
|
71520
|
-
getTrackIdForEvent
|
|
71979
|
+
listByOwner: listInfo,
|
|
71980
|
+
getTrackIdForEvent
|
|
71521
71981
|
}, id);
|
|
71522
|
-
const
|
|
71523
|
-
|
|
71524
|
-
return {
|
|
71525
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71526
|
-
key: chosen.key
|
|
71527
|
-
};
|
|
71982
|
+
const bodyKeys = new Set(layers.bodyFiles.map((f) => f.key));
|
|
71983
|
+
return await readWinningBlob([...layers.bodyFiles, ...layers.detailFiles], (rows) => resolveCropForcedMedia(rows.filter((r) => bodyKeys.has(r.key)), (files) => pickCleanMedia(files, preferKind), rows.filter((r) => !bodyKeys.has(r.key))), readBlob);
|
|
71528
71984
|
}
|
|
71529
|
-
const
|
|
71985
|
+
const { candidates, pick } = await collectDefaultEventMedia({
|
|
71530
71986
|
eventId: id,
|
|
71531
71987
|
eventFiles,
|
|
71532
|
-
listTrackMedia: (trackId) =>
|
|
71533
|
-
getTrackIdForEvent
|
|
71988
|
+
listTrackMedia: (trackId) => listInfo("track", trackId),
|
|
71989
|
+
getTrackIdForEvent
|
|
71534
71990
|
});
|
|
71535
|
-
|
|
71536
|
-
return {
|
|
71537
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71538
|
-
key: chosen.key
|
|
71539
|
-
};
|
|
71991
|
+
return await readWinningBlob(candidates, pick, readBlob);
|
|
71540
71992
|
}
|
|
71541
71993
|
globalSettingsSchema(cap) {
|
|
71542
71994
|
return this.schema(buildGlobalSettingsSchema(cap));
|