@camstack/addon-post-analysis 1.2.151 → 1.2.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-DpQno5NK.js → dist-Bw6cidiJ.js} +295 -193
- package/dist/{dist-B5K63Zmt.mjs → dist-DSg_jDMt.mjs} +295 -193
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-bK-ELdFD.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DTO1HvN7.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DxVeMmnD.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-CgUMPlDe.mjs → hostInit-r4RyRXk7.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +686 -474
- package/dist/pipeline-analytics/index.mjs +685 -473
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D8CtX8cU.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as discriminatedUnion, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as nodePin, G as TimelapseRulePatchSchema, Gt as record, H as SceneMonitorSchema, Ht as number, I as RECORDING_EXPORT_MAX_READ_BYTES, It as sleep, J as VISIT_MERGE_GAP_MS, Jt as EventCategory, K as TimelapseRuleSchema, Kt as string, L as RetrainStatusSchema, Lt as _enum, M as NcSystemEventKindSchema, Mt as createEvent, N as NcTaxonomySchema, Nt as hydrateSchema, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as isDeviceScopedCap, Q as audioMetricsCapability, Rt as array, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as object, V as SCENE_DIVERGED, Vt as literal, W as TimelapseRuleInputSchema, Wt as partialRecord, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Z as assertTimelapseCadences, _ as MACRO_LABELS, _t as pickClusterStepModels, a as COCO_TO_MACRO, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, ct as faceGalleryCapability, d as DeclaredDevices, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_KIND_BY_CAP, ft as isScheduleActive, g as LabelAttributionSchema, h as FailureCounters, ht as notificationRulesCapability, i as CLUSTER_MODEL_SCOPED_STEPS, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as DeviceType, k as NcSnoozeInputSchema, kt as errMsg, l as DETECTION_MACRO_CLASSES, lt as failureContributionCapability, m as FULL_IMAGE_BBOX, mt as kebabToCamel, n as AnalyticsGroupRecordSchema, nt as customAction, o as DEFAULT_EVENT_COLOR, ot as encodeVectorBase64, p as EVENT_PAD_MS, pt as isSourceCap, q as TrackSourceSchema, qt as unknown, r as BaseDevice, rt as defineCustomActions, st as evaluateSensorEdge, t as AnalyticsGroupDetailSchema, tt as cosineSimilarity$1, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as boolean } from "../dist-
|
|
1
|
+
import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as discriminatedUnion, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as nodePin, G as TimelapseRulePatchSchema, Gt as record, H as SceneMonitorSchema, Ht as number, I as RECORDING_EXPORT_MAX_READ_BYTES, It as sleep, J as VISIT_MERGE_GAP_MS, Jt as EventCategory, K as TimelapseRuleSchema, Kt as string, L as RetrainStatusSchema, Lt as _enum, M as NcSystemEventKindSchema, Mt as createEvent, N as NcTaxonomySchema, Nt as hydrateSchema, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as isDeviceScopedCap, Q as audioMetricsCapability, Rt as array, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as object, V as SCENE_DIVERGED, Vt as literal, W as TimelapseRuleInputSchema, Wt as partialRecord, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Z as assertTimelapseCadences, _ as MACRO_LABELS, _t as pickClusterStepModels, a as COCO_TO_MACRO, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, ct as faceGalleryCapability, d as DeclaredDevices, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_KIND_BY_CAP, ft as isScheduleActive, g as LabelAttributionSchema, h as FailureCounters, ht as notificationRulesCapability, i as CLUSTER_MODEL_SCOPED_STEPS, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as DeviceType, k as NcSnoozeInputSchema, kt as errMsg, l as DETECTION_MACRO_CLASSES, lt as failureContributionCapability, m as FULL_IMAGE_BBOX, mt as kebabToCamel, n as AnalyticsGroupRecordSchema, nt as customAction, o as DEFAULT_EVENT_COLOR, ot as encodeVectorBase64, p as EVENT_PAD_MS, pt as isSourceCap, q as TrackSourceSchema, qt as unknown, r as BaseDevice, rt as defineCustomActions, st as evaluateSensorEdge, t as AnalyticsGroupDetailSchema, tt as cosineSimilarity$1, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as boolean } from "../dist-DSg_jDMt.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -5406,12 +5406,6 @@ function buildAlarmButtons(input) {
|
|
|
5406
5406
|
}];
|
|
5407
5407
|
}
|
|
5408
5408
|
//#endregion
|
|
5409
|
-
//#region src/notification-center/cap-caller.ts
|
|
5410
|
-
function requireCapCaller(method, caller) {
|
|
5411
|
-
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`);
|
|
5412
|
-
return caller;
|
|
5413
|
-
}
|
|
5414
|
-
//#endregion
|
|
5415
5409
|
//#region src/notification-center/audio-rule-matcher.ts
|
|
5416
5410
|
/**
|
|
5417
5411
|
* AudioWatcher — the Notification Center's matcher for audio rules
|
|
@@ -5654,6 +5648,12 @@ var AudioWatcher = class {
|
|
|
5654
5648
|
}
|
|
5655
5649
|
};
|
|
5656
5650
|
//#endregion
|
|
5651
|
+
//#region src/notification-center/cap-caller.ts
|
|
5652
|
+
function requireCapCaller(method, caller) {
|
|
5653
|
+
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`);
|
|
5654
|
+
return caller;
|
|
5655
|
+
}
|
|
5656
|
+
//#endregion
|
|
5657
5657
|
//#region src/shared/llm-vision/prompt-hygiene.ts
|
|
5658
5658
|
/**
|
|
5659
5659
|
* What a PIPELINE VALUE is allowed to look like once it is inside a prompt.
|
|
@@ -6288,25 +6288,46 @@ var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
|
6288
6288
|
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
6289
6289
|
var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
|
|
6290
6290
|
/**
|
|
6291
|
-
* Kinds whose action is "go and look at
|
|
6291
|
+
* Kinds whose action is "go and look at this device's stream".
|
|
6292
|
+
*
|
|
6293
|
+
* BOTH DIRECTIONS. A recovery lands on the same page as the failure it undoes,
|
|
6294
|
+
* because "did it really come back, and what did it do while it was gone" is
|
|
6295
|
+
* an action and the page that answers it is the one the down-notification
|
|
6296
|
+
* already opened. On 2026-08-30 the operator's phone showed "Il dispositivo
|
|
6297
|
+
* Videocamera sala feste è online" with `tapAction.value: null` — the same
|
|
6298
|
+
* camera's `device-offline` an hour earlier opened the Stream Broker tab, and
|
|
6299
|
+
* the recovery opened nothing, purely because `device-online` had no entry
|
|
6300
|
+
* here.
|
|
6292
6301
|
*
|
|
6293
|
-
* The legacy `camera
|
|
6294
|
-
* enum: outbox and history rows enqueued before the rename carry
|
|
6302
|
+
* The legacy `camera-*` pair is here for the same reason both are still in the
|
|
6303
|
+
* enum: outbox and history rows enqueued before the rename carry them, and an
|
|
6295
6304
|
* old row is exactly the one whose action has to keep working.
|
|
6305
|
+
*
|
|
6306
|
+
* Kinds NOT here are kinds whose DOWN direction has no action either
|
|
6307
|
+
* (`node-online`/`node-offline`, `device-enabled`/`device-disabled`) — a
|
|
6308
|
+
* recovery does not earn a destination its failure was never given.
|
|
6296
6309
|
*/
|
|
6297
|
-
var
|
|
6310
|
+
var DEVICE_STREAM_KINDS = new Set([
|
|
6298
6311
|
"device-offline",
|
|
6312
|
+
"device-online",
|
|
6299
6313
|
"stream-offline",
|
|
6300
|
-
"
|
|
6314
|
+
"stream-online",
|
|
6315
|
+
"camera-offline",
|
|
6316
|
+
"camera-online"
|
|
6301
6317
|
]);
|
|
6302
6318
|
/**
|
|
6303
6319
|
* Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
|
|
6304
6320
|
*
|
|
6305
|
-
* A stream belongs to a camera and nothing else. The legacy `camera
|
|
6306
|
-
*
|
|
6307
|
-
*
|
|
6321
|
+
* A stream belongs to a camera and nothing else. The legacy `camera-*` pair is
|
|
6322
|
+
* too — that name is precisely the claim it made, and it is why the kind was
|
|
6323
|
+
* split into `device-*` + a `deviceTypes` condition.
|
|
6308
6324
|
*/
|
|
6309
|
-
var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
|
|
6325
|
+
var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
|
|
6326
|
+
"stream-offline",
|
|
6327
|
+
"stream-online",
|
|
6328
|
+
"camera-offline",
|
|
6329
|
+
"camera-online"
|
|
6330
|
+
]);
|
|
6310
6331
|
function withParam(path, key, value) {
|
|
6311
6332
|
if (value === void 0 || value.length === 0) return path;
|
|
6312
6333
|
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
@@ -6321,7 +6342,7 @@ function withParam(path, key, value) {
|
|
|
6321
6342
|
function systemEventTapPath(subject) {
|
|
6322
6343
|
if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
|
|
6323
6344
|
if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
|
|
6324
|
-
if (
|
|
6345
|
+
if (DEVICE_STREAM_KINDS.has(subject.kind)) {
|
|
6325
6346
|
const deviceId = subject.deviceId;
|
|
6326
6347
|
if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
6327
6348
|
const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
|
|
@@ -10723,16 +10744,26 @@ function incomingFromUpdateAvailable(facts, data) {
|
|
|
10723
10744
|
...nodeIds.length > 0 ? { nodeIds } : {}
|
|
10724
10745
|
});
|
|
10725
10746
|
}
|
|
10726
|
-
/**
|
|
10747
|
+
/**
|
|
10748
|
+
* Version-bearing `addon.updated` only — a settings save carries no versions
|
|
10749
|
+
* at all (`BaseAddon.updateGlobalSettings`).
|
|
10750
|
+
*
|
|
10751
|
+
* `toVersion` IS the discriminator, and `fromVersion` is not: the emitter
|
|
10752
|
+
* omits the predecessor when it cannot read what was installed (a first
|
|
10753
|
+
* install, an unreadable tree). Requiring both dropped those updates on the
|
|
10754
|
+
* floor entirely — the operator got no notification for an update that
|
|
10755
|
+
* happened — which is the price that was being paid to keep `'0.0.0'`
|
|
10756
|
+
* out. Report the half that is known; say nothing about the half that is not.
|
|
10757
|
+
*/
|
|
10727
10758
|
function parseAddonUpdateComplete(data) {
|
|
10728
10759
|
const packageName = stringField(data, "packageName");
|
|
10729
10760
|
const fromVersion = stringField(data, "fromVersion");
|
|
10730
10761
|
const toVersion = stringField(data, "toVersion");
|
|
10731
|
-
if (packageName === void 0 ||
|
|
10762
|
+
if (packageName === void 0 || toVersion === void 0) return null;
|
|
10732
10763
|
const nodeId = stringField(data, "agent") ?? stringField(data, "nodeId");
|
|
10733
10764
|
return {
|
|
10734
10765
|
packageName,
|
|
10735
|
-
fromVersion,
|
|
10766
|
+
...fromVersion !== void 0 ? { fromVersion } : {},
|
|
10736
10767
|
toVersion,
|
|
10737
10768
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10738
10769
|
};
|
|
@@ -10742,7 +10773,10 @@ function incomingFromAddonUpdates(facts, updates) {
|
|
|
10742
10773
|
const packageNames = unique(updates.map((update) => update.packageName));
|
|
10743
10774
|
const nodeIds = unique(updates.map((update) => update.nodeId).filter((id) => id !== void 0));
|
|
10744
10775
|
const signature = updates.map((update) => `${update.nodeId ?? ""}:${update.packageName}@${update.toVersion}`).toSorted().join(",");
|
|
10745
|
-
const list = updates.map((update) =>
|
|
10776
|
+
const list = updates.map((update) => {
|
|
10777
|
+
const transition = update.fromVersion !== void 0 ? `${update.fromVersion} → ${update.toVersion}` : `→ ${update.toVersion}`;
|
|
10778
|
+
return update.nodeId !== void 0 ? `${update.packageName} ${transition} (${update.nodeId})` : `${update.packageName} ${transition}`;
|
|
10779
|
+
}).join("\n");
|
|
10746
10780
|
const head = updates[0];
|
|
10747
10781
|
return systemSubject({
|
|
10748
10782
|
facts,
|
|
@@ -23564,17 +23598,17 @@ function planRuleTest(input) {
|
|
|
23564
23598
|
deviceName: `camera ${String(systemScope.deviceId ?? deviceId)}`,
|
|
23565
23599
|
nodeId: systemScope.nodeId ?? "hub",
|
|
23566
23600
|
packageName: systemScope.packageName ?? "@camstack/server",
|
|
23567
|
-
currentVersion: "
|
|
23568
|
-
latestVersion: "
|
|
23601
|
+
currentVersion: "1.0.0",
|
|
23602
|
+
latestVersion: "1.0.1",
|
|
23569
23603
|
profile: "low",
|
|
23570
23604
|
seconds: "30",
|
|
23571
23605
|
devices: "cuda:0",
|
|
23572
23606
|
minutes: "15",
|
|
23573
23607
|
addons: TEST_CRASHED_RUNNER,
|
|
23574
|
-
list: "@camstack/addon-pipeline 1.0.
|
|
23608
|
+
list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
|
|
23575
23609
|
nodes: "hub",
|
|
23576
|
-
fromVersion: "
|
|
23577
|
-
toVersion: "
|
|
23610
|
+
fromVersion: "1.0.0",
|
|
23611
|
+
toVersion: "1.0.1",
|
|
23578
23612
|
title: "Export",
|
|
23579
23613
|
count: "1"
|
|
23580
23614
|
},
|
|
@@ -25724,7 +25758,7 @@ var MediaStore = class {
|
|
|
25724
25758
|
});
|
|
25725
25759
|
}
|
|
25726
25760
|
/** Persist a media blob and register its metadata row. Returns the
|
|
25727
|
-
* collection key (
|
|
25761
|
+
* collection key (MediaFileRef.key) that callers store as reference. */
|
|
25728
25762
|
async put(params) {
|
|
25729
25763
|
if (this.writeGate) return this.writeGate.run(() => this.putUnchecked(params));
|
|
25730
25764
|
return this.putUnchecked(params);
|
|
@@ -26046,8 +26080,20 @@ var MediaStore = class {
|
|
|
26046
26080
|
* its device at write time, so scoping the query by it makes a media read
|
|
26047
26081
|
* for a device the caller cannot name return nothing — the DB itself
|
|
26048
26082
|
* enforces the boundary, no post-hoc ownership check to forget.
|
|
26083
|
+
*
|
|
26084
|
+
* `kinds` is the WORK filter, and it belongs here rather than on the answer.
|
|
26085
|
+
* Every row this returns has had its blob read off disk and base64-encoded,
|
|
26086
|
+
* so a caller that filters afterwards has already paid for every kind it is
|
|
26087
|
+
* about to discard: measured on the live hub, one track's six kinds are
|
|
26088
|
+
* 3 597 219 bytes and `thumbnailSmall` is 31 155 of them, and asking for the
|
|
26089
|
+
* small one alone cost exactly as much as asking for all six. `kind` is a
|
|
26090
|
+
* real column (see {@link MEDIA_COLUMNS}), so the narrowing happens in the
|
|
26091
|
+
* query and the blobs of the other kinds are never opened.
|
|
26092
|
+
*
|
|
26093
|
+
* Absent (or empty) means every kind — the back-compat default, and the
|
|
26094
|
+
* honest answer for a caller that has not decided what it needs.
|
|
26049
26095
|
*/
|
|
26050
|
-
async listByOwner(ownerKind, ownerId, deviceId) {
|
|
26096
|
+
async listByOwner(ownerKind, ownerId, deviceId, kinds) {
|
|
26051
26097
|
const rows = await this.store.query.query({
|
|
26052
26098
|
collection: MEDIA_COLLECTION,
|
|
26053
26099
|
filter: {
|
|
@@ -26059,6 +26105,7 @@ var MediaStore = class {
|
|
|
26059
26105
|
ownerId,
|
|
26060
26106
|
deviceId
|
|
26061
26107
|
},
|
|
26108
|
+
...kinds !== void 0 && kinds.length > 0 ? { whereIn: { kind: [...kinds] } } : {},
|
|
26062
26109
|
orderBy: {
|
|
26063
26110
|
field: "timestamp",
|
|
26064
26111
|
direction: "asc"
|
|
@@ -27974,6 +28021,63 @@ var embeddingActions = defineCustomActions({
|
|
|
27974
28021
|
"embedding.reembedFacesStatus": customAction(NoInputSchema$1, ReembedFacesStatusSchema)
|
|
27975
28022
|
});
|
|
27976
28023
|
//#endregion
|
|
28024
|
+
//#region src/pipeline-analytics/media-file-url.ts
|
|
28025
|
+
/**
|
|
28026
|
+
* The plane path for one stored media key.
|
|
28027
|
+
*
|
|
28028
|
+
* `prefix` is `/addon/<addonId>/event-media` — a pure function of the addon
|
|
28029
|
+
* id, so it exists whether or not `dataPlane.serve` succeeded. A row that
|
|
28030
|
+
* named no address at all would be the silent drop this repo keeps paying for:
|
|
28031
|
+
* a client cannot tell "no bytes" from "no plane", and renders neither.
|
|
28032
|
+
*/
|
|
28033
|
+
function mediaFileUrl(prefix, key) {
|
|
28034
|
+
return `${prefix}/${encodeURIComponent(key)}`;
|
|
28035
|
+
}
|
|
28036
|
+
/**
|
|
28037
|
+
* A stored row as the wire sees it: the ref, the address, and — transitionally
|
|
28038
|
+
* — the bytes.
|
|
28039
|
+
*
|
|
28040
|
+
* `base64` is copied through ONLY because the deployed viewer's track-detail
|
|
28041
|
+
* hero still requires the field to parse a row at all. See the `MediaFile`
|
|
28042
|
+
* docblock in `pipeline-analytics.cap.ts`; deleting the field is a one-line
|
|
28043
|
+
* change here and one in the schema.
|
|
28044
|
+
*
|
|
28045
|
+
* `provenance` is deliberately NOT spread: it is store-internal, and the
|
|
28046
|
+
* previous `{ ...row, kind }` shape published it by accident.
|
|
28047
|
+
*/
|
|
28048
|
+
function toWireMediaFile(row, prefix) {
|
|
28049
|
+
return {
|
|
28050
|
+
key: row.key,
|
|
28051
|
+
kind: row.kind,
|
|
28052
|
+
sizeBytes: row.sizeBytes,
|
|
28053
|
+
timestamp: row.timestamp,
|
|
28054
|
+
url: mediaFileUrl(prefix, row.key),
|
|
28055
|
+
base64: row.base64
|
|
28056
|
+
};
|
|
28057
|
+
}
|
|
28058
|
+
/** The same row without its bytes — the manifest shape. Carries the SAME
|
|
28059
|
+
* `url` as the byte read, so a client never builds the plane path itself. */
|
|
28060
|
+
function toWireMediaFileInfo(row, prefix) {
|
|
28061
|
+
return {
|
|
28062
|
+
key: row.key,
|
|
28063
|
+
kind: row.kind,
|
|
28064
|
+
sizeBytes: row.sizeBytes,
|
|
28065
|
+
timestamp: row.timestamp,
|
|
28066
|
+
url: mediaFileUrl(prefix, row.key)
|
|
28067
|
+
};
|
|
28068
|
+
}
|
|
28069
|
+
//#endregion
|
|
28070
|
+
//#region src/pipeline-analytics/gallery-media-row.ts
|
|
28071
|
+
function toGalleryMediaRow(row, prefix) {
|
|
28072
|
+
return {
|
|
28073
|
+
key: row.key,
|
|
28074
|
+
kind: row.kind,
|
|
28075
|
+
url: mediaFileUrl(prefix, row.key),
|
|
28076
|
+
sizeBytes: row.sizeBytes,
|
|
28077
|
+
timestamp: row.timestamp
|
|
28078
|
+
};
|
|
28079
|
+
}
|
|
28080
|
+
//#endregion
|
|
27977
28081
|
//#region src/pipeline-analytics/include-crops-default.ts
|
|
27978
28082
|
function resolveIncludeCrops(input) {
|
|
27979
28083
|
return input?.includeCrops === true;
|
|
@@ -29633,6 +29737,7 @@ var FaceGalleryProvider = class {
|
|
|
29633
29737
|
logger;
|
|
29634
29738
|
refreshGallery;
|
|
29635
29739
|
eventMediaBaseUrl;
|
|
29740
|
+
eventMediaPathPrefix;
|
|
29636
29741
|
emitFaceGalleryChanged;
|
|
29637
29742
|
constructor(deps) {
|
|
29638
29743
|
this.identityStore = deps.identityStore;
|
|
@@ -29643,6 +29748,7 @@ var FaceGalleryProvider = class {
|
|
|
29643
29748
|
this.logger = deps.logger;
|
|
29644
29749
|
this.refreshGallery = deps.refreshGallery;
|
|
29645
29750
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
29751
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
29646
29752
|
this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
|
|
29647
29753
|
}
|
|
29648
29754
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
@@ -29830,13 +29936,7 @@ var FaceGalleryProvider = class {
|
|
|
29830
29936
|
* the enrolled identity copies — those live under identity owner).
|
|
29831
29937
|
*/
|
|
29832
29938
|
async getFaceMedia(input) {
|
|
29833
|
-
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => (
|
|
29834
|
-
key: m.key,
|
|
29835
|
-
kind: m.kind,
|
|
29836
|
-
base64: m.base64,
|
|
29837
|
-
sizeBytes: m.sizeBytes,
|
|
29838
|
-
timestamp: m.timestamp
|
|
29839
|
-
}));
|
|
29939
|
+
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
29840
29940
|
}
|
|
29841
29941
|
/**
|
|
29842
29942
|
* Assign a buffered face to an identity:
|
|
@@ -32547,50 +32647,136 @@ var FrameProcessor = class {
|
|
|
32547
32647
|
};
|
|
32548
32648
|
}
|
|
32549
32649
|
};
|
|
32550
|
-
|
|
32551
|
-
|
|
32650
|
+
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
32651
|
+
* evicts the oldest record. */
|
|
32652
|
+
var PendingPreviewUpgrades = class {
|
|
32653
|
+
entries = /* @__PURE__ */ new Map();
|
|
32654
|
+
ttlMs;
|
|
32655
|
+
maxEntries;
|
|
32656
|
+
now;
|
|
32657
|
+
constructor(options = {}) {
|
|
32658
|
+
this.ttlMs = options.ttlMs ?? 9e5;
|
|
32659
|
+
this.maxEntries = Math.max(1, options.maxEntries ?? 512);
|
|
32660
|
+
this.now = options.now ?? (() => Date.now());
|
|
32661
|
+
}
|
|
32662
|
+
/** Record (or replace) a closed track's pending upgrade. Prunes expired
|
|
32663
|
+
* entries first, then evicts the oldest while over the cap. */
|
|
32664
|
+
record(entry) {
|
|
32665
|
+
this.prune();
|
|
32666
|
+
this.entries.delete(entry.trackId);
|
|
32667
|
+
this.entries.set(entry.trackId, entry);
|
|
32668
|
+
while (this.entries.size > this.maxEntries) {
|
|
32669
|
+
const oldest = this.entries.keys().next();
|
|
32670
|
+
if (oldest.done === true) break;
|
|
32671
|
+
this.entries.delete(oldest.value);
|
|
32672
|
+
}
|
|
32673
|
+
}
|
|
32674
|
+
/** Claim a track's pending upgrade — removes it, so a second landing (or a
|
|
32675
|
+
* concurrent racer) finds nothing and the tile is rewritten at most once. */
|
|
32676
|
+
take(trackId) {
|
|
32677
|
+
this.prune();
|
|
32678
|
+
const entry = this.entries.get(trackId);
|
|
32679
|
+
if (entry === void 0) return void 0;
|
|
32680
|
+
this.entries.delete(trackId);
|
|
32681
|
+
return entry;
|
|
32682
|
+
}
|
|
32683
|
+
get size() {
|
|
32684
|
+
return this.entries.size;
|
|
32685
|
+
}
|
|
32686
|
+
prune() {
|
|
32687
|
+
const cutoff = this.now() - this.ttlMs;
|
|
32688
|
+
for (const [trackId, entry] of this.entries) if (entry.recordedAtMs < cutoff) this.entries.delete(trackId);
|
|
32689
|
+
}
|
|
32690
|
+
};
|
|
32552
32691
|
/**
|
|
32553
|
-
*
|
|
32554
|
-
*
|
|
32555
|
-
*
|
|
32556
|
-
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
32557
|
-
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
32692
|
+
* Redeem a closed track's pending preview off a keyFrame that landed late.
|
|
32693
|
+
* Every non-`upgraded` outcome that had a pending entry logs itself — a
|
|
32694
|
+
* branch that leaves the stand-in in place must say so.
|
|
32558
32695
|
*/
|
|
32559
|
-
function
|
|
32560
|
-
const
|
|
32561
|
-
|
|
32562
|
-
if (
|
|
32563
|
-
|
|
32564
|
-
|
|
32565
|
-
|
|
32566
|
-
|
|
32567
|
-
|
|
32568
|
-
frame
|
|
32696
|
+
async function runPreviewUpgrade(deps, input) {
|
|
32697
|
+
const { deviceId, trackId } = input;
|
|
32698
|
+
const pending = deps.takePending(trackId);
|
|
32699
|
+
if (pending === void 0) return "no-pending";
|
|
32700
|
+
const held = await deps.heldThumbnailProvenance(trackId);
|
|
32701
|
+
if (held !== null && mediaProvenanceRank(held) >= mediaProvenanceRank("native")) {
|
|
32702
|
+
deps.logger.info("late keyFrame: preview already native — no upgrade", {
|
|
32703
|
+
tags: { deviceId },
|
|
32704
|
+
meta: { trackId }
|
|
32569
32705
|
});
|
|
32570
|
-
|
|
32571
|
-
|
|
32572
|
-
|
|
32573
|
-
|
|
32574
|
-
|
|
32575
|
-
|
|
32576
|
-
|
|
32577
|
-
|
|
32578
|
-
framesMatched: matched
|
|
32579
|
-
});
|
|
32580
|
-
else {
|
|
32581
|
-
existing.className = td.className;
|
|
32582
|
-
existing.lastSeenMs = timestamp;
|
|
32583
|
-
existing.framesMatched += matched;
|
|
32706
|
+
return "held-native";
|
|
32707
|
+
}
|
|
32708
|
+
if (pending.bestSeen === void 0) {
|
|
32709
|
+
deps.logger.info("late keyFrame landed but no subject bbox was ever seen — tile kept as-is", {
|
|
32710
|
+
tags: { deviceId },
|
|
32711
|
+
meta: {
|
|
32712
|
+
trackId,
|
|
32713
|
+
heldProvenance: held
|
|
32584
32714
|
}
|
|
32585
|
-
}
|
|
32715
|
+
});
|
|
32716
|
+
return "no-bbox";
|
|
32586
32717
|
}
|
|
32587
|
-
|
|
32588
|
-
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
|
|
32592
|
-
|
|
32593
|
-
|
|
32718
|
+
if (pending.bestSeen.timestamp === void 0) {
|
|
32719
|
+
deps.logger.info("late keyFrame: pending entry predates the motion guard — tile kept as-is", {
|
|
32720
|
+
tags: { deviceId },
|
|
32721
|
+
meta: {
|
|
32722
|
+
trackId,
|
|
32723
|
+
keyFrameKey: input.keyFrameKey,
|
|
32724
|
+
heldProvenance: held
|
|
32725
|
+
}
|
|
32726
|
+
});
|
|
32727
|
+
return "no-bbox";
|
|
32728
|
+
}
|
|
32729
|
+
const derived = await deps.derive({
|
|
32730
|
+
deviceId,
|
|
32731
|
+
mediaKey: input.keyFrameKey,
|
|
32732
|
+
bbox: {
|
|
32733
|
+
x: pending.bestSeen.x,
|
|
32734
|
+
y: pending.bestSeen.y,
|
|
32735
|
+
w: pending.bestSeen.w,
|
|
32736
|
+
h: pending.bestSeen.h
|
|
32737
|
+
},
|
|
32738
|
+
frameWidth: pending.bestSeen.frameWidth,
|
|
32739
|
+
frameHeight: pending.bestSeen.frameHeight,
|
|
32740
|
+
timestamp: pending.bestSeen.timestamp
|
|
32741
|
+
});
|
|
32742
|
+
if (derived === null) {
|
|
32743
|
+
deps.logger.info("late keyFrame preview upgrade failed (derive miss) — tile kept as-is", {
|
|
32744
|
+
tags: { deviceId },
|
|
32745
|
+
meta: {
|
|
32746
|
+
trackId,
|
|
32747
|
+
keyFrameKey: input.keyFrameKey,
|
|
32748
|
+
heldProvenance: held
|
|
32749
|
+
}
|
|
32750
|
+
});
|
|
32751
|
+
return "derive-miss";
|
|
32752
|
+
}
|
|
32753
|
+
await deps.put({
|
|
32754
|
+
deviceId,
|
|
32755
|
+
trackId,
|
|
32756
|
+
kind: "thumbnail",
|
|
32757
|
+
timestamp: input.timestamp,
|
|
32758
|
+
data: derived.thumbnail,
|
|
32759
|
+
provenance: "native"
|
|
32760
|
+
});
|
|
32761
|
+
await deps.put({
|
|
32762
|
+
deviceId,
|
|
32763
|
+
trackId,
|
|
32764
|
+
kind: "thumbnailSmall",
|
|
32765
|
+
timestamp: input.timestamp,
|
|
32766
|
+
data: derived.thumbnailSmall,
|
|
32767
|
+
provenance: "native"
|
|
32768
|
+
});
|
|
32769
|
+
await deps.clearPreviewMiss(deviceId, trackId);
|
|
32770
|
+
deps.logger.info("best-shot upgraded from late keyFrame (stand-in replaced)", {
|
|
32771
|
+
tags: { deviceId },
|
|
32772
|
+
meta: {
|
|
32773
|
+
trackId,
|
|
32774
|
+
keyFrameKey: input.keyFrameKey,
|
|
32775
|
+
replaced: held ?? "none",
|
|
32776
|
+
skewMs: derived.skewMs
|
|
32777
|
+
}
|
|
32778
|
+
});
|
|
32779
|
+
return "upgraded";
|
|
32594
32780
|
}
|
|
32595
32781
|
function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, score) {
|
|
32596
32782
|
if (frameWidth <= 0 || frameHeight <= 0) return true;
|
|
@@ -32750,137 +32936,6 @@ function promoteSemanticBestFrame(deps, input) {
|
|
|
32750
32936
|
captured: true
|
|
32751
32937
|
};
|
|
32752
32938
|
}
|
|
32753
|
-
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
32754
|
-
* evicts the oldest record. */
|
|
32755
|
-
var PendingPreviewUpgrades = class {
|
|
32756
|
-
entries = /* @__PURE__ */ new Map();
|
|
32757
|
-
ttlMs;
|
|
32758
|
-
maxEntries;
|
|
32759
|
-
now;
|
|
32760
|
-
constructor(options = {}) {
|
|
32761
|
-
this.ttlMs = options.ttlMs ?? 9e5;
|
|
32762
|
-
this.maxEntries = Math.max(1, options.maxEntries ?? 512);
|
|
32763
|
-
this.now = options.now ?? (() => Date.now());
|
|
32764
|
-
}
|
|
32765
|
-
/** Record (or replace) a closed track's pending upgrade. Prunes expired
|
|
32766
|
-
* entries first, then evicts the oldest while over the cap. */
|
|
32767
|
-
record(entry) {
|
|
32768
|
-
this.prune();
|
|
32769
|
-
this.entries.delete(entry.trackId);
|
|
32770
|
-
this.entries.set(entry.trackId, entry);
|
|
32771
|
-
while (this.entries.size > this.maxEntries) {
|
|
32772
|
-
const oldest = this.entries.keys().next();
|
|
32773
|
-
if (oldest.done === true) break;
|
|
32774
|
-
this.entries.delete(oldest.value);
|
|
32775
|
-
}
|
|
32776
|
-
}
|
|
32777
|
-
/** Claim a track's pending upgrade — removes it, so a second landing (or a
|
|
32778
|
-
* concurrent racer) finds nothing and the tile is rewritten at most once. */
|
|
32779
|
-
take(trackId) {
|
|
32780
|
-
this.prune();
|
|
32781
|
-
const entry = this.entries.get(trackId);
|
|
32782
|
-
if (entry === void 0) return void 0;
|
|
32783
|
-
this.entries.delete(trackId);
|
|
32784
|
-
return entry;
|
|
32785
|
-
}
|
|
32786
|
-
get size() {
|
|
32787
|
-
return this.entries.size;
|
|
32788
|
-
}
|
|
32789
|
-
prune() {
|
|
32790
|
-
const cutoff = this.now() - this.ttlMs;
|
|
32791
|
-
for (const [trackId, entry] of this.entries) if (entry.recordedAtMs < cutoff) this.entries.delete(trackId);
|
|
32792
|
-
}
|
|
32793
|
-
};
|
|
32794
|
-
/**
|
|
32795
|
-
* Redeem a closed track's pending preview off a keyFrame that landed late.
|
|
32796
|
-
* Every non-`upgraded` outcome that had a pending entry logs itself — a
|
|
32797
|
-
* branch that leaves the stand-in in place must say so.
|
|
32798
|
-
*/
|
|
32799
|
-
async function runPreviewUpgrade(deps, input) {
|
|
32800
|
-
const { deviceId, trackId } = input;
|
|
32801
|
-
const pending = deps.takePending(trackId);
|
|
32802
|
-
if (pending === void 0) return "no-pending";
|
|
32803
|
-
const held = await deps.heldThumbnailProvenance(trackId);
|
|
32804
|
-
if (held !== null && mediaProvenanceRank(held) >= mediaProvenanceRank("native")) {
|
|
32805
|
-
deps.logger.info("late keyFrame: preview already native — no upgrade", {
|
|
32806
|
-
tags: { deviceId },
|
|
32807
|
-
meta: { trackId }
|
|
32808
|
-
});
|
|
32809
|
-
return "held-native";
|
|
32810
|
-
}
|
|
32811
|
-
if (pending.bestSeen === void 0) {
|
|
32812
|
-
deps.logger.info("late keyFrame landed but no subject bbox was ever seen — tile kept as-is", {
|
|
32813
|
-
tags: { deviceId },
|
|
32814
|
-
meta: {
|
|
32815
|
-
trackId,
|
|
32816
|
-
heldProvenance: held
|
|
32817
|
-
}
|
|
32818
|
-
});
|
|
32819
|
-
return "no-bbox";
|
|
32820
|
-
}
|
|
32821
|
-
if (pending.bestSeen.timestamp === void 0) {
|
|
32822
|
-
deps.logger.info("late keyFrame: pending entry predates the motion guard — tile kept as-is", {
|
|
32823
|
-
tags: { deviceId },
|
|
32824
|
-
meta: {
|
|
32825
|
-
trackId,
|
|
32826
|
-
keyFrameKey: input.keyFrameKey,
|
|
32827
|
-
heldProvenance: held
|
|
32828
|
-
}
|
|
32829
|
-
});
|
|
32830
|
-
return "no-bbox";
|
|
32831
|
-
}
|
|
32832
|
-
const derived = await deps.derive({
|
|
32833
|
-
deviceId,
|
|
32834
|
-
mediaKey: input.keyFrameKey,
|
|
32835
|
-
bbox: {
|
|
32836
|
-
x: pending.bestSeen.x,
|
|
32837
|
-
y: pending.bestSeen.y,
|
|
32838
|
-
w: pending.bestSeen.w,
|
|
32839
|
-
h: pending.bestSeen.h
|
|
32840
|
-
},
|
|
32841
|
-
frameWidth: pending.bestSeen.frameWidth,
|
|
32842
|
-
frameHeight: pending.bestSeen.frameHeight,
|
|
32843
|
-
timestamp: pending.bestSeen.timestamp
|
|
32844
|
-
});
|
|
32845
|
-
if (derived === null) {
|
|
32846
|
-
deps.logger.info("late keyFrame preview upgrade failed (derive miss) — tile kept as-is", {
|
|
32847
|
-
tags: { deviceId },
|
|
32848
|
-
meta: {
|
|
32849
|
-
trackId,
|
|
32850
|
-
keyFrameKey: input.keyFrameKey,
|
|
32851
|
-
heldProvenance: held
|
|
32852
|
-
}
|
|
32853
|
-
});
|
|
32854
|
-
return "derive-miss";
|
|
32855
|
-
}
|
|
32856
|
-
await deps.put({
|
|
32857
|
-
deviceId,
|
|
32858
|
-
trackId,
|
|
32859
|
-
kind: "thumbnail",
|
|
32860
|
-
timestamp: input.timestamp,
|
|
32861
|
-
data: derived.thumbnail,
|
|
32862
|
-
provenance: "native"
|
|
32863
|
-
});
|
|
32864
|
-
await deps.put({
|
|
32865
|
-
deviceId,
|
|
32866
|
-
trackId,
|
|
32867
|
-
kind: "thumbnailSmall",
|
|
32868
|
-
timestamp: input.timestamp,
|
|
32869
|
-
data: derived.thumbnailSmall,
|
|
32870
|
-
provenance: "native"
|
|
32871
|
-
});
|
|
32872
|
-
await deps.clearPreviewMiss(deviceId, trackId);
|
|
32873
|
-
deps.logger.info("best-shot upgraded from late keyFrame (stand-in replaced)", {
|
|
32874
|
-
tags: { deviceId },
|
|
32875
|
-
meta: {
|
|
32876
|
-
trackId,
|
|
32877
|
-
keyFrameKey: input.keyFrameKey,
|
|
32878
|
-
replaced: held ?? "none",
|
|
32879
|
-
skewMs: derived.skewMs
|
|
32880
|
-
}
|
|
32881
|
-
});
|
|
32882
|
-
return "upgraded";
|
|
32883
|
-
}
|
|
32884
32939
|
//#endregion
|
|
32885
32940
|
//#region src/pipeline-analytics/store/zone-geometry.ts
|
|
32886
32941
|
/**
|
|
@@ -34619,6 +34674,7 @@ var PlateGalleryProvider = class {
|
|
|
34619
34674
|
refreshGallery;
|
|
34620
34675
|
emitPlateGalleryChanged;
|
|
34621
34676
|
eventMediaBaseUrl;
|
|
34677
|
+
eventMediaPathPrefix;
|
|
34622
34678
|
constructor(deps) {
|
|
34623
34679
|
this.plateStore = deps.plateStore;
|
|
34624
34680
|
this.vehicleStore = deps.vehicleStore;
|
|
@@ -34629,6 +34685,7 @@ var PlateGalleryProvider = class {
|
|
|
34629
34685
|
this.refreshGallery = deps.refreshGallery;
|
|
34630
34686
|
this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
|
|
34631
34687
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
34688
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
34632
34689
|
}
|
|
34633
34690
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
34634
34691
|
safeEmitPlateGalleryChanged(payload) {
|
|
@@ -34699,13 +34756,7 @@ var PlateGalleryProvider = class {
|
|
|
34699
34756
|
return this.toPlateInfo(plate, nameMap, { includeCrops: false });
|
|
34700
34757
|
}
|
|
34701
34758
|
async getPlateMedia(input) {
|
|
34702
|
-
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => (
|
|
34703
|
-
key: m.key,
|
|
34704
|
-
kind: m.kind,
|
|
34705
|
-
base64: m.base64,
|
|
34706
|
-
sizeBytes: m.sizeBytes,
|
|
34707
|
-
timestamp: m.timestamp
|
|
34708
|
-
}));
|
|
34759
|
+
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
34709
34760
|
}
|
|
34710
34761
|
async searchPlates(input) {
|
|
34711
34762
|
const maxDistance = input.maxDistance ?? 1;
|
|
@@ -34948,6 +34999,51 @@ var PlateGalleryProvider = class {
|
|
|
34948
34999
|
}
|
|
34949
35000
|
};
|
|
34950
35001
|
//#endregion
|
|
35002
|
+
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
35003
|
+
/**
|
|
35004
|
+
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
35005
|
+
* return the tracks it produced.
|
|
35006
|
+
*
|
|
35007
|
+
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
35008
|
+
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
35009
|
+
*/
|
|
35010
|
+
function runFrameProcessorPass(input) {
|
|
35011
|
+
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
35012
|
+
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
35013
|
+
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
35014
|
+
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
35015
|
+
const accum = /* @__PURE__ */ new Map();
|
|
35016
|
+
for (const { timestamp, frame } of input.frames) {
|
|
35017
|
+
const result = processor.process({
|
|
35018
|
+
timestamp,
|
|
35019
|
+
frame
|
|
35020
|
+
});
|
|
35021
|
+
for (const td of result.tracked) {
|
|
35022
|
+
const existing = accum.get(td.trackId);
|
|
35023
|
+
const matched = td.matchedThisFrame ? 1 : 0;
|
|
35024
|
+
if (existing === void 0) accum.set(td.trackId, {
|
|
35025
|
+
className: td.className,
|
|
35026
|
+
firstSeenMs: timestamp,
|
|
35027
|
+
lastSeenMs: timestamp,
|
|
35028
|
+
bbox: { ...td.bbox },
|
|
35029
|
+
framesMatched: matched
|
|
35030
|
+
});
|
|
35031
|
+
else {
|
|
35032
|
+
existing.className = td.className;
|
|
35033
|
+
existing.lastSeenMs = timestamp;
|
|
35034
|
+
existing.framesMatched += matched;
|
|
35035
|
+
}
|
|
35036
|
+
}
|
|
35037
|
+
}
|
|
35038
|
+
return { tracks: [...accum.values()].map((t) => ({
|
|
35039
|
+
className: t.className,
|
|
35040
|
+
firstSeenMs: t.firstSeenMs,
|
|
35041
|
+
lastSeenMs: t.lastSeenMs,
|
|
35042
|
+
bbox: t.bbox,
|
|
35043
|
+
framesMatched: t.framesMatched
|
|
35044
|
+
})) };
|
|
35045
|
+
}
|
|
35046
|
+
//#endregion
|
|
34951
35047
|
//#region src/pipeline-analytics/retention/disk-reconcile.ts
|
|
34952
35048
|
/**
|
|
34953
35049
|
* Disk-wins reconcile for pipeline analytics.
|
|
@@ -35012,92 +35108,6 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
35012
35108
|
};
|
|
35013
35109
|
}
|
|
35014
35110
|
//#endregion
|
|
35015
|
-
//#region src/pipeline-analytics/retention/media-reclaim-actions.ts
|
|
35016
|
-
/**
|
|
35017
|
-
* The operator button for the debug-media reclaim.
|
|
35018
|
-
*
|
|
35019
|
-
* The mechanism ships armed with nothing: **arming it is the operator's
|
|
35020
|
-
* call**. `mode` omitted means `report` — it counts, logs one line per camera
|
|
35021
|
-
* and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
|
|
35022
|
-
* that has to be typed.
|
|
35023
|
-
*
|
|
35024
|
-
* Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
|
|
35025
|
-
* earned it: dry-run default, explicit pacing, explicit blast radius, and a
|
|
35026
|
-
* report you can read instead of a log line you have to find.
|
|
35027
|
-
*/
|
|
35028
|
-
/**
|
|
35029
|
-
* `report` counts; `reclaim` deletes.
|
|
35030
|
-
*
|
|
35031
|
-
* `.default('report')` is load-bearing: the settings form sends `{}` for a
|
|
35032
|
-
* button with no fields, and the safe reading of an unstated intent is the one
|
|
35033
|
-
* that destroys nothing.
|
|
35034
|
-
*/
|
|
35035
|
-
var MediaReclaimActionModeSchema = _enum(["report", "reclaim"]).default("report");
|
|
35036
|
-
var MediaReclaimScopeSchema = _enum(["motion-still", "track-filmstrip"]);
|
|
35037
|
-
var MediaReclaimActionInputSchema = object({
|
|
35038
|
-
mode: MediaReclaimActionModeSchema,
|
|
35039
|
-
/** Which populations to walk. Omitted = both. */
|
|
35040
|
-
scopes: array(MediaReclaimScopeSchema).min(1).optional(),
|
|
35041
|
-
/** Restrict to these cameras. Omitted = every camera in retention scope. */
|
|
35042
|
-
deviceIds: array(number().int()).min(1).optional(),
|
|
35043
|
-
/** Start every device from the top instead of resuming its cursor. */
|
|
35044
|
-
restart: boolean().optional(),
|
|
35045
|
-
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
35046
|
-
pageSize: number().int().min(50).max(5e3).optional(),
|
|
35047
|
-
/** Rows EXAMINED per camera in this pass — the pacing bound. */
|
|
35048
|
-
maxRowsPerDevice: number().int().min(1).max(5e5).optional(),
|
|
35049
|
-
/** Rows DELETED per camera in this pass — the blast-radius bound. */
|
|
35050
|
-
maxReclaimPerDevice: number().int().min(1).max(5e5).optional(),
|
|
35051
|
-
/** Bytes released across the whole run — the disk-contention bound. */
|
|
35052
|
-
maxBytesPerRun: number().int().min(1).optional(),
|
|
35053
|
-
/** Wall-clock ceiling for the run. */
|
|
35054
|
-
budgetMinutes: number().int().min(1).max(720).optional(),
|
|
35055
|
-
/**
|
|
35056
|
-
* Bytes per second the pass may release. The recordings volume sustains
|
|
35057
|
-
* ~4 MB/s and the archive needs it more than this does; the default is half
|
|
35058
|
-
* of that.
|
|
35059
|
-
*/
|
|
35060
|
-
throttleBytesPerSec: number().int().min(64 * 1024).optional(),
|
|
35061
|
-
/**
|
|
35062
|
-
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
35063
|
-
* exclusively save rows, never select them.
|
|
35064
|
-
*/
|
|
35065
|
-
graceMinutes: number().int().min(1).max(10080).optional()
|
|
35066
|
-
});
|
|
35067
|
-
var MediaReclaimDeviceReportSchema = object({
|
|
35068
|
-
deviceId: number(),
|
|
35069
|
-
scope: MediaReclaimScopeSchema,
|
|
35070
|
-
examined: number(),
|
|
35071
|
-
eligible: number(),
|
|
35072
|
-
reclaimed: number(),
|
|
35073
|
-
bytesReclaimed: number(),
|
|
35074
|
-
/** Rows kept because the owning track is pinned (`staging`/`favourited`). */
|
|
35075
|
-
held: number(),
|
|
35076
|
-
/** Rows kept because the owner could not be shown to be a motion event. */
|
|
35077
|
-
unresolved: number(),
|
|
35078
|
-
withinGrace: number(),
|
|
35079
|
-
/** Deletes that REFUSED — the blob survived or the volume could not answer.
|
|
35080
|
-
* The row was kept and the cursor did not pass it. */
|
|
35081
|
-
refused: number(),
|
|
35082
|
-
stoppedBy: string(),
|
|
35083
|
-
/** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
|
|
35084
|
-
nextCursor: number()
|
|
35085
|
-
});
|
|
35086
|
-
var mediaReclaimActions = defineCustomActions({ "retention.mediaReclaim": customAction(MediaReclaimActionInputSchema, object({
|
|
35087
|
-
/** `false` when a pass was already in flight — nothing was done. */
|
|
35088
|
-
ran: boolean(),
|
|
35089
|
-
mode: _enum(["report", "reclaim"]),
|
|
35090
|
-
devices: array(MediaReclaimDeviceReportSchema),
|
|
35091
|
-
totalExamined: number(),
|
|
35092
|
-
totalEligible: number(),
|
|
35093
|
-
totalReclaimed: number(),
|
|
35094
|
-
totalBytesReclaimed: number(),
|
|
35095
|
-
totalRefused: number(),
|
|
35096
|
-
durationMs: number(),
|
|
35097
|
-
/** Every device+scope reported `exhausted` — there is nothing left to do. */
|
|
35098
|
-
complete: boolean()
|
|
35099
|
-
}), { auth: "admin" }) });
|
|
35100
|
-
//#endregion
|
|
35101
35111
|
//#region src/pipeline-analytics/retention/media-reclaim.ts
|
|
35102
35112
|
var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
|
|
35103
35113
|
var DEFAULT_MEDIA_RECLAIM_LIMITS = {
|
|
@@ -35308,6 +35318,92 @@ async function runMediaReclaim(input) {
|
|
|
35308
35318
|
cursors: nextCursors
|
|
35309
35319
|
};
|
|
35310
35320
|
}
|
|
35321
|
+
//#endregion
|
|
35322
|
+
//#region src/pipeline-analytics/retention/media-reclaim-actions.ts
|
|
35323
|
+
/**
|
|
35324
|
+
* The operator button for the debug-media reclaim.
|
|
35325
|
+
*
|
|
35326
|
+
* The mechanism ships armed with nothing: **arming it is the operator's
|
|
35327
|
+
* call**. `mode` omitted means `report` — it counts, logs one line per camera
|
|
35328
|
+
* and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
|
|
35329
|
+
* that has to be typed.
|
|
35330
|
+
*
|
|
35331
|
+
* Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
|
|
35332
|
+
* earned it: dry-run default, explicit pacing, explicit blast radius, and a
|
|
35333
|
+
* report you can read instead of a log line you have to find.
|
|
35334
|
+
*/
|
|
35335
|
+
/**
|
|
35336
|
+
* `report` counts; `reclaim` deletes.
|
|
35337
|
+
*
|
|
35338
|
+
* `.default('report')` is load-bearing: the settings form sends `{}` for a
|
|
35339
|
+
* button with no fields, and the safe reading of an unstated intent is the one
|
|
35340
|
+
* that destroys nothing.
|
|
35341
|
+
*/
|
|
35342
|
+
var MediaReclaimActionModeSchema = _enum(["report", "reclaim"]).default("report");
|
|
35343
|
+
var MediaReclaimScopeSchema = _enum(["motion-still", "track-filmstrip"]);
|
|
35344
|
+
var MediaReclaimActionInputSchema = object({
|
|
35345
|
+
mode: MediaReclaimActionModeSchema,
|
|
35346
|
+
/** Which populations to walk. Omitted = both. */
|
|
35347
|
+
scopes: array(MediaReclaimScopeSchema).min(1).optional(),
|
|
35348
|
+
/** Restrict to these cameras. Omitted = every camera in retention scope. */
|
|
35349
|
+
deviceIds: array(number().int()).min(1).optional(),
|
|
35350
|
+
/** Start every device from the top instead of resuming its cursor. */
|
|
35351
|
+
restart: boolean().optional(),
|
|
35352
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
35353
|
+
pageSize: number().int().min(50).max(5e3).optional(),
|
|
35354
|
+
/** Rows EXAMINED per camera in this pass — the pacing bound. */
|
|
35355
|
+
maxRowsPerDevice: number().int().min(1).max(5e5).optional(),
|
|
35356
|
+
/** Rows DELETED per camera in this pass — the blast-radius bound. */
|
|
35357
|
+
maxReclaimPerDevice: number().int().min(1).max(5e5).optional(),
|
|
35358
|
+
/** Bytes released across the whole run — the disk-contention bound. */
|
|
35359
|
+
maxBytesPerRun: number().int().min(1).optional(),
|
|
35360
|
+
/** Wall-clock ceiling for the run. */
|
|
35361
|
+
budgetMinutes: number().int().min(1).max(720).optional(),
|
|
35362
|
+
/**
|
|
35363
|
+
* Bytes per second the pass may release. The recordings volume sustains
|
|
35364
|
+
* ~4 MB/s and the archive needs it more than this does; the default is half
|
|
35365
|
+
* of that.
|
|
35366
|
+
*/
|
|
35367
|
+
throttleBytesPerSec: number().int().min(64 * 1024).optional(),
|
|
35368
|
+
/**
|
|
35369
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
35370
|
+
* exclusively save rows, never select them.
|
|
35371
|
+
*/
|
|
35372
|
+
graceMinutes: number().int().min(1).max(10080).optional()
|
|
35373
|
+
});
|
|
35374
|
+
var MediaReclaimDeviceReportSchema = object({
|
|
35375
|
+
deviceId: number(),
|
|
35376
|
+
scope: MediaReclaimScopeSchema,
|
|
35377
|
+
examined: number(),
|
|
35378
|
+
eligible: number(),
|
|
35379
|
+
reclaimed: number(),
|
|
35380
|
+
bytesReclaimed: number(),
|
|
35381
|
+
/** Rows kept because the owning track is pinned (`staging`/`favourited`). */
|
|
35382
|
+
held: number(),
|
|
35383
|
+
/** Rows kept because the owner could not be shown to be a motion event. */
|
|
35384
|
+
unresolved: number(),
|
|
35385
|
+
withinGrace: number(),
|
|
35386
|
+
/** Deletes that REFUSED — the blob survived or the volume could not answer.
|
|
35387
|
+
* The row was kept and the cursor did not pass it. */
|
|
35388
|
+
refused: number(),
|
|
35389
|
+
stoppedBy: string(),
|
|
35390
|
+
/** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
|
|
35391
|
+
nextCursor: number()
|
|
35392
|
+
});
|
|
35393
|
+
var mediaReclaimActions = defineCustomActions({ "retention.mediaReclaim": customAction(MediaReclaimActionInputSchema, object({
|
|
35394
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
35395
|
+
ran: boolean(),
|
|
35396
|
+
mode: _enum(["report", "reclaim"]),
|
|
35397
|
+
devices: array(MediaReclaimDeviceReportSchema),
|
|
35398
|
+
totalExamined: number(),
|
|
35399
|
+
totalEligible: number(),
|
|
35400
|
+
totalReclaimed: number(),
|
|
35401
|
+
totalBytesReclaimed: number(),
|
|
35402
|
+
totalRefused: number(),
|
|
35403
|
+
durationMs: number(),
|
|
35404
|
+
/** Every device+scope reported `exhausted` — there is nothing left to do. */
|
|
35405
|
+
complete: boolean()
|
|
35406
|
+
}), { auth: "admin" }) });
|
|
35311
35407
|
var OrphanAuditActionInputSchema = object({
|
|
35312
35408
|
mode: _enum(["report", "reclaim"]).default("report"),
|
|
35313
35409
|
/**
|
|
@@ -36627,6 +36723,28 @@ function trackMediaScopesForKinds(trackId, kinds) {
|
|
|
36627
36723
|
return kinds.some((kind) => DETAIL_CROP_KINDS.includes(kind)) ? scopes : scopes.filter((scope) => scope.publishCropAs === null);
|
|
36628
36724
|
}
|
|
36629
36725
|
/**
|
|
36726
|
+
* The STORED kinds worth reading from one scope for a `kinds` request, or
|
|
36727
|
+
* `null` when the scope cannot contribute to it at all.
|
|
36728
|
+
*
|
|
36729
|
+
* This is the same vocabulary translation {@link collectTrackMedia} performs on
|
|
36730
|
+
* the way out, moved to the way IN so it can narrow the query instead of the
|
|
36731
|
+
* answer. A caller naming `faceCrop` must ask the face owner for its stored
|
|
36732
|
+
* `crop` — asking it for `faceCrop` matches nothing, and asking it for
|
|
36733
|
+
* everything opens blobs the answer will discard.
|
|
36734
|
+
*
|
|
36735
|
+
* `undefined` means "every kind": the honest translation of an unfiltered
|
|
36736
|
+
* request, and the only safe answer for the open-ended track scope.
|
|
36737
|
+
*
|
|
36738
|
+
* `null` (skip) is only ever returned for a DETAIL owner whose one publishable
|
|
36739
|
+
* kind was not asked for. The track scope is never skipped — its kinds are
|
|
36740
|
+
* open-ended, and dropping it on a guess is how a listing starts lying.
|
|
36741
|
+
*/
|
|
36742
|
+
function storedKindsForScope(scope, kinds) {
|
|
36743
|
+
if (kinds === void 0 || kinds.length === 0) return void 0;
|
|
36744
|
+
if (scope.publishCropAs === null) return kinds;
|
|
36745
|
+
return kinds.includes(scope.publishCropAs) ? [STORED_DETAIL_CROP_KIND] : null;
|
|
36746
|
+
}
|
|
36747
|
+
/**
|
|
36630
36748
|
* Fold every scope's rows into ONE track media listing: detail crops
|
|
36631
36749
|
* relabelled, everything filtered to `kinds`, ordered oldest first.
|
|
36632
36750
|
*
|
|
@@ -36763,8 +36881,10 @@ var AnalyticsQueryFacade = class {
|
|
|
36763
36881
|
return this.withMediaUrl(events);
|
|
36764
36882
|
}
|
|
36765
36883
|
async getEventMedia(input) {
|
|
36766
|
-
const
|
|
36767
|
-
|
|
36884
|
+
const store = this.deps.mediaStore();
|
|
36885
|
+
if (store === null) return [];
|
|
36886
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36887
|
+
return (await store.listByOwner("event", input.eventId, input.deviceId, input.kind === void 0 ? void 0 : [input.kind])).map((row) => toWireMediaFile(row, prefix));
|
|
36768
36888
|
}
|
|
36769
36889
|
/**
|
|
36770
36890
|
* A track's media WITH the bytes, restricted to `kinds` when given.
|
|
@@ -36777,14 +36897,36 @@ var AnalyticsQueryFacade = class {
|
|
|
36777
36897
|
async getTrackMedia(input) {
|
|
36778
36898
|
const store = this.deps.mediaStore();
|
|
36779
36899
|
if (store === null) return [];
|
|
36900
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36780
36901
|
const scopes = trackMediaScopesForKinds(input.trackId, input.kinds);
|
|
36781
|
-
return collectTrackMedia(await Promise.all(scopes.map(async (scope) =>
|
|
36782
|
-
scope,
|
|
36783
|
-
|
|
36784
|
-
|
|
36902
|
+
return collectTrackMedia(await Promise.all(scopes.map(async (scope) => {
|
|
36903
|
+
const stored = storedKindsForScope(scope, input.kinds);
|
|
36904
|
+
if (stored === null) return {
|
|
36905
|
+
scope,
|
|
36906
|
+
rows: []
|
|
36907
|
+
};
|
|
36908
|
+
return {
|
|
36909
|
+
scope,
|
|
36910
|
+
rows: await store.listByOwner(scope.ownerKind, scope.ownerId, input.deviceId, stored)
|
|
36911
|
+
};
|
|
36912
|
+
})), (row, kind) => ({
|
|
36785
36913
|
...row,
|
|
36786
36914
|
kind
|
|
36787
|
-
}), input.kinds);
|
|
36915
|
+
}), input.kinds).map((row) => toWireMediaFile(row, prefix));
|
|
36916
|
+
}
|
|
36917
|
+
/**
|
|
36918
|
+
* An event's media WITHOUT the bytes.
|
|
36919
|
+
*
|
|
36920
|
+
* Index rows only — it never opens a blob. The track detail modal unions
|
|
36921
|
+
* this with {@link listTrackMedia} to build its filmstrip and then fetches
|
|
36922
|
+
* each tile from the `event-media` plane by key, so the bytes this used to
|
|
36923
|
+
* carry were read, base64'd, shipped and discarded.
|
|
36924
|
+
*/
|
|
36925
|
+
async listEventMedia(input) {
|
|
36926
|
+
const store = this.deps.mediaStore();
|
|
36927
|
+
if (store === null) return [];
|
|
36928
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36929
|
+
return (await store.listInfoByOwner("event", input.eventId, input.deviceId)).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36788
36930
|
}
|
|
36789
36931
|
/**
|
|
36790
36932
|
* A track's media WITHOUT the bytes.
|
|
@@ -36802,13 +36944,14 @@ var AnalyticsQueryFacade = class {
|
|
|
36802
36944
|
async listTrackMedia(input) {
|
|
36803
36945
|
const store = this.deps.mediaStore();
|
|
36804
36946
|
if (store === null) return [];
|
|
36947
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36805
36948
|
return collectTrackMedia(await Promise.all(trackMediaOwnerScopes(input.trackId).map(async (scope) => ({
|
|
36806
36949
|
scope,
|
|
36807
36950
|
rows: await store.listInfoByOwner(scope.ownerKind, scope.ownerId, input.deviceId)
|
|
36808
36951
|
}))), (row, kind) => ({
|
|
36809
36952
|
...row,
|
|
36810
36953
|
kind
|
|
36811
|
-
}), void 0);
|
|
36954
|
+
}), void 0).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36812
36955
|
}
|
|
36813
36956
|
async pruneEventsBefore(input) {
|
|
36814
36957
|
const eventStore = this.deps.eventStore();
|
|
@@ -39253,6 +39396,31 @@ function createEventMediaHandler(deps) {
|
|
|
39253
39396
|
}
|
|
39254
39397
|
//#endregion
|
|
39255
39398
|
//#region src/pipeline-analytics/event-thumbnail-resolver.ts
|
|
39399
|
+
/**
|
|
39400
|
+
* Rank `candidates` with `pick`, then open ONE blob — the winner's.
|
|
39401
|
+
*
|
|
39402
|
+
* The ladders decide on `kind` alone, so the bytes are dead weight until the
|
|
39403
|
+
* choice is made. Handing them pre-read rows meant a tile that serves the
|
|
39404
|
+
* 31 KB `thumbnailSmall` first read the whole 3 597 219-byte set, including a
|
|
39405
|
+
* 2.8 MB `keyFrame` no list view has ever rendered.
|
|
39406
|
+
*
|
|
39407
|
+
* A winner whose blob is GONE is dropped and the ladder re-run on the rest.
|
|
39408
|
+
* That fall-through is not an extra: `listByOwner` used to provide it for free
|
|
39409
|
+
* by skipping unreadable rows before the ladder ever saw them, and without it
|
|
39410
|
+
* an index row that outlived its file would 404 a tile that used to render.
|
|
39411
|
+
* Bounded by the candidate count — each attempt removes one row.
|
|
39412
|
+
*/
|
|
39413
|
+
async function readWinningBlob(candidates, pick, readBlob) {
|
|
39414
|
+
let remaining = candidates;
|
|
39415
|
+
while (remaining.length > 0) {
|
|
39416
|
+
const winner = pick(remaining);
|
|
39417
|
+
if (winner === void 0) return null;
|
|
39418
|
+
const bytes = await readBlob(winner.key);
|
|
39419
|
+
if (bytes !== null) return bytes;
|
|
39420
|
+
remaining = remaining.filter((row) => row.key !== winner.key);
|
|
39421
|
+
}
|
|
39422
|
+
return null;
|
|
39423
|
+
}
|
|
39256
39424
|
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
39257
39425
|
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
39258
39426
|
* for new tracks (served only for old stored rows). Undefined when the event
|
|
@@ -39303,15 +39471,24 @@ async function collectCropForcedMedia(source, id) {
|
|
|
39303
39471
|
};
|
|
39304
39472
|
}
|
|
39305
39473
|
/**
|
|
39306
|
-
*
|
|
39307
|
-
* has any, else the owning track's
|
|
39308
|
-
*
|
|
39474
|
+
* Which rows the default path should serve from, and how to rank them: the
|
|
39475
|
+
* event's OWN media when it has any, else the owning track's.
|
|
39476
|
+
*
|
|
39477
|
+
* It yields the candidate SET rather than a winner so the caller can open one
|
|
39478
|
+
* blob and, if that blob has gone, re-rank the rest ({@link readWinningBlob}).
|
|
39479
|
+
* Returning a single row would force the caller to read it blind and 404 a tile
|
|
39480
|
+
* whose index row outlived its file.
|
|
39309
39481
|
*/
|
|
39310
|
-
async function
|
|
39311
|
-
|
|
39312
|
-
|
|
39482
|
+
async function collectDefaultEventMedia(deps) {
|
|
39483
|
+
if (deps.eventFiles.length > 0) return {
|
|
39484
|
+
candidates: deps.eventFiles,
|
|
39485
|
+
pick: pickEventOwnedMedia
|
|
39486
|
+
};
|
|
39313
39487
|
const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
|
|
39314
|
-
return
|
|
39488
|
+
return {
|
|
39489
|
+
candidates: await deps.listTrackMedia(trackId),
|
|
39490
|
+
pick: pickTrackFallbackMedia
|
|
39491
|
+
};
|
|
39315
39492
|
}
|
|
39316
39493
|
//#endregion
|
|
39317
39494
|
//#region src/pipeline-analytics/face-settings.ts
|
|
@@ -47418,6 +47595,70 @@ var PackageDropDetector = class {
|
|
|
47418
47595
|
}
|
|
47419
47596
|
};
|
|
47420
47597
|
//#endregion
|
|
47598
|
+
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
47599
|
+
/**
|
|
47600
|
+
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
47601
|
+
*
|
|
47602
|
+
* 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
|
|
47603
|
+
* routinely pokes a little outside the vehicle box, and requiring total
|
|
47604
|
+
* containment would count almost none of them. 0.8 and not 0.5: half a person
|
|
47605
|
+
* box overlapping a car is what a pedestrian walking in front of one looks
|
|
47606
|
+
* like, and counting those would make the number mean "somebody was near a
|
|
47607
|
+
* car", which is not a defect.
|
|
47608
|
+
*/
|
|
47609
|
+
var CONTAINMENT_THRESHOLD = .8;
|
|
47610
|
+
/**
|
|
47611
|
+
* The classes that count as a vehicle for this test.
|
|
47612
|
+
*
|
|
47613
|
+
* A local list rather than a shared taxonomy on purpose: it exists to describe
|
|
47614
|
+
* ONE measurement, and a shared enum would make a change here silently change
|
|
47615
|
+
* detection behaviour somewhere else.
|
|
47616
|
+
*/
|
|
47617
|
+
var VEHICLE_CLASSES$1 = [
|
|
47618
|
+
"car",
|
|
47619
|
+
"truck",
|
|
47620
|
+
"bus",
|
|
47621
|
+
"motorcycle",
|
|
47622
|
+
"vehicle"
|
|
47623
|
+
];
|
|
47624
|
+
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
47625
|
+
function intersectionArea(a, b) {
|
|
47626
|
+
const left = Math.max(a.x, b.x);
|
|
47627
|
+
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
47628
|
+
const top = Math.max(a.y, b.y);
|
|
47629
|
+
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
47630
|
+
if (right <= left || bottom <= top) return 0;
|
|
47631
|
+
return (right - left) * (bottom - top);
|
|
47632
|
+
}
|
|
47633
|
+
/**
|
|
47634
|
+
* Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
|
|
47635
|
+
*
|
|
47636
|
+
* `0` for a degenerate person box: a zero-area box is inside everything and
|
|
47637
|
+
* inside nothing, and answering `1` would count every one of them.
|
|
47638
|
+
*/
|
|
47639
|
+
function containedFraction(person, vehicle) {
|
|
47640
|
+
const area = person.w * person.h;
|
|
47641
|
+
if (area <= 0) return 0;
|
|
47642
|
+
return intersectionArea(person, vehicle) / area;
|
|
47643
|
+
}
|
|
47644
|
+
/**
|
|
47645
|
+
* Is this person box contained in any vehicle box in the same frame?
|
|
47646
|
+
*
|
|
47647
|
+
* Returns the fraction for the BEST-containing vehicle when it clears
|
|
47648
|
+
* {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
|
|
47649
|
+
* a counter that only ever says "yes" cannot later be re-thresholded against
|
|
47650
|
+
* what was actually measured.
|
|
47651
|
+
*/
|
|
47652
|
+
function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
|
|
47653
|
+
let best = 0;
|
|
47654
|
+
for (const neighbour of neighbours) {
|
|
47655
|
+
if (!VEHICLE_CLASSES$1.includes(neighbour.className)) continue;
|
|
47656
|
+
const fraction = containedFraction(person, neighbour.bbox);
|
|
47657
|
+
if (fraction > best) best = fraction;
|
|
47658
|
+
}
|
|
47659
|
+
return best >= threshold ? best : null;
|
|
47660
|
+
}
|
|
47661
|
+
//#endregion
|
|
47421
47662
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
|
|
47422
47663
|
/**
|
|
47423
47664
|
* stationary-evidence — how a parked object EARNS a registry entry.
|
|
@@ -49949,83 +50190,6 @@ function buildRecordedStillPorts(input) {
|
|
|
49949
50190
|
})
|
|
49950
50191
|
};
|
|
49951
50192
|
}
|
|
49952
|
-
function errorMessage(err) {
|
|
49953
|
-
return err instanceof Error ? err.message : String(err);
|
|
49954
|
-
}
|
|
49955
|
-
async function reconcileGallerySamples(deps, options) {
|
|
49956
|
-
let checked = 0;
|
|
49957
|
-
let reclaimed = 0;
|
|
49958
|
-
let keyless = 0;
|
|
49959
|
-
let skippedGrace = 0;
|
|
49960
|
-
let stoppedByCeiling = false;
|
|
49961
|
-
const identities = await deps.listIdentities();
|
|
49962
|
-
for (const identity of identities) {
|
|
49963
|
-
const samples = await deps.listSamples(identity.id);
|
|
49964
|
-
for (const sample of samples) {
|
|
49965
|
-
if (sample.mediaKey === void 0) {
|
|
49966
|
-
keyless += 1;
|
|
49967
|
-
continue;
|
|
49968
|
-
}
|
|
49969
|
-
if (options.now - sample.addedAt < options.graceMs) {
|
|
49970
|
-
skippedGrace += 1;
|
|
49971
|
-
continue;
|
|
49972
|
-
}
|
|
49973
|
-
if (reclaimed >= options.maxReclaimPerRun) {
|
|
49974
|
-
stoppedByCeiling = true;
|
|
49975
|
-
deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
|
|
49976
|
-
return {
|
|
49977
|
-
checked,
|
|
49978
|
-
reclaimed,
|
|
49979
|
-
keyless,
|
|
49980
|
-
skippedGrace,
|
|
49981
|
-
stoppedByCeiling
|
|
49982
|
-
};
|
|
49983
|
-
}
|
|
49984
|
-
let state;
|
|
49985
|
-
try {
|
|
49986
|
-
state = await deps.blobState(sample.mediaKey);
|
|
49987
|
-
} catch (err) {
|
|
49988
|
-
const reason = errorMessage(err);
|
|
49989
|
-
deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
|
|
49990
|
-
sampleId: sample.id,
|
|
49991
|
-
identity: identity.name,
|
|
49992
|
-
error: reason
|
|
49993
|
-
} });
|
|
49994
|
-
return {
|
|
49995
|
-
checked,
|
|
49996
|
-
reclaimed,
|
|
49997
|
-
keyless,
|
|
49998
|
-
skippedGrace,
|
|
49999
|
-
stoppedByCeiling,
|
|
50000
|
-
aborted: reason
|
|
50001
|
-
};
|
|
50002
|
-
}
|
|
50003
|
-
checked += 1;
|
|
50004
|
-
if (state === "exists") continue;
|
|
50005
|
-
await deps.removeSample(identity.id, sample.id);
|
|
50006
|
-
reclaimed += 1;
|
|
50007
|
-
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
50008
|
-
sampleId: sample.id,
|
|
50009
|
-
identity: identity.name,
|
|
50010
|
-
mediaKey: sample.mediaKey,
|
|
50011
|
-
state
|
|
50012
|
-
} });
|
|
50013
|
-
}
|
|
50014
|
-
}
|
|
50015
|
-
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
50016
|
-
checked,
|
|
50017
|
-
reclaimed,
|
|
50018
|
-
keyless,
|
|
50019
|
-
skippedGrace
|
|
50020
|
-
} });
|
|
50021
|
-
return {
|
|
50022
|
-
checked,
|
|
50023
|
-
reclaimed,
|
|
50024
|
-
keyless,
|
|
50025
|
-
skippedGrace,
|
|
50026
|
-
stoppedByCeiling
|
|
50027
|
-
};
|
|
50028
|
-
}
|
|
50029
50193
|
//#endregion
|
|
50030
50194
|
//#region src/pipeline-analytics/retention/embedding-orphan-scan.ts
|
|
50031
50195
|
/** The scope id this leg reports and persists its cursor under. */
|
|
@@ -50142,6 +50306,83 @@ async function scanEmbeddingOrphans(deps, limits, cursor) {
|
|
|
50142
50306
|
nextCursor: Math.max(0, offset)
|
|
50143
50307
|
};
|
|
50144
50308
|
}
|
|
50309
|
+
function errorMessage(err) {
|
|
50310
|
+
return err instanceof Error ? err.message : String(err);
|
|
50311
|
+
}
|
|
50312
|
+
async function reconcileGallerySamples(deps, options) {
|
|
50313
|
+
let checked = 0;
|
|
50314
|
+
let reclaimed = 0;
|
|
50315
|
+
let keyless = 0;
|
|
50316
|
+
let skippedGrace = 0;
|
|
50317
|
+
let stoppedByCeiling = false;
|
|
50318
|
+
const identities = await deps.listIdentities();
|
|
50319
|
+
for (const identity of identities) {
|
|
50320
|
+
const samples = await deps.listSamples(identity.id);
|
|
50321
|
+
for (const sample of samples) {
|
|
50322
|
+
if (sample.mediaKey === void 0) {
|
|
50323
|
+
keyless += 1;
|
|
50324
|
+
continue;
|
|
50325
|
+
}
|
|
50326
|
+
if (options.now - sample.addedAt < options.graceMs) {
|
|
50327
|
+
skippedGrace += 1;
|
|
50328
|
+
continue;
|
|
50329
|
+
}
|
|
50330
|
+
if (reclaimed >= options.maxReclaimPerRun) {
|
|
50331
|
+
stoppedByCeiling = true;
|
|
50332
|
+
deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
|
|
50333
|
+
return {
|
|
50334
|
+
checked,
|
|
50335
|
+
reclaimed,
|
|
50336
|
+
keyless,
|
|
50337
|
+
skippedGrace,
|
|
50338
|
+
stoppedByCeiling
|
|
50339
|
+
};
|
|
50340
|
+
}
|
|
50341
|
+
let state;
|
|
50342
|
+
try {
|
|
50343
|
+
state = await deps.blobState(sample.mediaKey);
|
|
50344
|
+
} catch (err) {
|
|
50345
|
+
const reason = errorMessage(err);
|
|
50346
|
+
deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
|
|
50347
|
+
sampleId: sample.id,
|
|
50348
|
+
identity: identity.name,
|
|
50349
|
+
error: reason
|
|
50350
|
+
} });
|
|
50351
|
+
return {
|
|
50352
|
+
checked,
|
|
50353
|
+
reclaimed,
|
|
50354
|
+
keyless,
|
|
50355
|
+
skippedGrace,
|
|
50356
|
+
stoppedByCeiling,
|
|
50357
|
+
aborted: reason
|
|
50358
|
+
};
|
|
50359
|
+
}
|
|
50360
|
+
checked += 1;
|
|
50361
|
+
if (state === "exists") continue;
|
|
50362
|
+
await deps.removeSample(identity.id, sample.id);
|
|
50363
|
+
reclaimed += 1;
|
|
50364
|
+
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
50365
|
+
sampleId: sample.id,
|
|
50366
|
+
identity: identity.name,
|
|
50367
|
+
mediaKey: sample.mediaKey,
|
|
50368
|
+
state
|
|
50369
|
+
} });
|
|
50370
|
+
}
|
|
50371
|
+
}
|
|
50372
|
+
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
50373
|
+
checked,
|
|
50374
|
+
reclaimed,
|
|
50375
|
+
keyless,
|
|
50376
|
+
skippedGrace
|
|
50377
|
+
} });
|
|
50378
|
+
return {
|
|
50379
|
+
checked,
|
|
50380
|
+
reclaimed,
|
|
50381
|
+
keyless,
|
|
50382
|
+
skippedGrace,
|
|
50383
|
+
stoppedByCeiling
|
|
50384
|
+
};
|
|
50385
|
+
}
|
|
50145
50386
|
/**
|
|
50146
50387
|
* Where the completion marker lives.
|
|
50147
50388
|
*
|
|
@@ -53204,14 +53445,14 @@ function isAfterCursor(row, cursor) {
|
|
|
53204
53445
|
if (row.lastSeen < cursor.lastSeen) return true;
|
|
53205
53446
|
return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
|
|
53206
53447
|
}
|
|
53207
|
-
var VEHICLE_CLASSES
|
|
53448
|
+
var VEHICLE_CLASSES = new Set([
|
|
53208
53449
|
"vehicle",
|
|
53209
53450
|
"car",
|
|
53210
53451
|
"truck",
|
|
53211
53452
|
"bus"
|
|
53212
53453
|
]);
|
|
53213
53454
|
function motionlessMaxPxForClass(className) {
|
|
53214
|
-
return VEHICLE_CLASSES
|
|
53455
|
+
return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
|
|
53215
53456
|
}
|
|
53216
53457
|
function netDisplacementPx(positions) {
|
|
53217
53458
|
const first = positions[0];
|
|
@@ -56971,70 +57212,6 @@ function hasWrappedDetection(entries) {
|
|
|
56971
57212
|
return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
|
|
56972
57213
|
}
|
|
56973
57214
|
//#endregion
|
|
56974
|
-
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
56975
|
-
/**
|
|
56976
|
-
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
56977
|
-
*
|
|
56978
|
-
* 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
|
|
56979
|
-
* routinely pokes a little outside the vehicle box, and requiring total
|
|
56980
|
-
* containment would count almost none of them. 0.8 and not 0.5: half a person
|
|
56981
|
-
* box overlapping a car is what a pedestrian walking in front of one looks
|
|
56982
|
-
* like, and counting those would make the number mean "somebody was near a
|
|
56983
|
-
* car", which is not a defect.
|
|
56984
|
-
*/
|
|
56985
|
-
var CONTAINMENT_THRESHOLD = .8;
|
|
56986
|
-
/**
|
|
56987
|
-
* The classes that count as a vehicle for this test.
|
|
56988
|
-
*
|
|
56989
|
-
* A local list rather than a shared taxonomy on purpose: it exists to describe
|
|
56990
|
-
* ONE measurement, and a shared enum would make a change here silently change
|
|
56991
|
-
* detection behaviour somewhere else.
|
|
56992
|
-
*/
|
|
56993
|
-
var VEHICLE_CLASSES = [
|
|
56994
|
-
"car",
|
|
56995
|
-
"truck",
|
|
56996
|
-
"bus",
|
|
56997
|
-
"motorcycle",
|
|
56998
|
-
"vehicle"
|
|
56999
|
-
];
|
|
57000
|
-
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
57001
|
-
function intersectionArea(a, b) {
|
|
57002
|
-
const left = Math.max(a.x, b.x);
|
|
57003
|
-
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
57004
|
-
const top = Math.max(a.y, b.y);
|
|
57005
|
-
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
57006
|
-
if (right <= left || bottom <= top) return 0;
|
|
57007
|
-
return (right - left) * (bottom - top);
|
|
57008
|
-
}
|
|
57009
|
-
/**
|
|
57010
|
-
* Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
|
|
57011
|
-
*
|
|
57012
|
-
* `0` for a degenerate person box: a zero-area box is inside everything and
|
|
57013
|
-
* inside nothing, and answering `1` would count every one of them.
|
|
57014
|
-
*/
|
|
57015
|
-
function containedFraction(person, vehicle) {
|
|
57016
|
-
const area = person.w * person.h;
|
|
57017
|
-
if (area <= 0) return 0;
|
|
57018
|
-
return intersectionArea(person, vehicle) / area;
|
|
57019
|
-
}
|
|
57020
|
-
/**
|
|
57021
|
-
* Is this person box contained in any vehicle box in the same frame?
|
|
57022
|
-
*
|
|
57023
|
-
* Returns the fraction for the BEST-containing vehicle when it clears
|
|
57024
|
-
* {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
|
|
57025
|
-
* a counter that only ever says "yes" cannot later be re-thresholded against
|
|
57026
|
-
* what was actually measured.
|
|
57027
|
-
*/
|
|
57028
|
-
function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
|
|
57029
|
-
let best = 0;
|
|
57030
|
-
for (const neighbour of neighbours) {
|
|
57031
|
-
if (!VEHICLE_CLASSES.includes(neighbour.className)) continue;
|
|
57032
|
-
const fraction = containedFraction(person, neighbour.bbox);
|
|
57033
|
-
if (fraction > best) best = fraction;
|
|
57034
|
-
}
|
|
57035
|
-
return best >= threshold ? best : null;
|
|
57036
|
-
}
|
|
57037
|
-
//#endregion
|
|
57038
57215
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
57039
57216
|
function createCaptureCrop(deps) {
|
|
57040
57217
|
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
@@ -63476,6 +63653,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
63476
63653
|
* Set once the data-plane is registered; null until then (e.g. no
|
|
63477
63654
|
* dataPlane facility in the current environment). */
|
|
63478
63655
|
eventMediaBaseUrl = null;
|
|
63656
|
+
/**
|
|
63657
|
+
* The SAME path, derived rather than observed — a pure function of the addon
|
|
63658
|
+
* id, so it exists before `serve()` is attempted and survives its failure.
|
|
63659
|
+
*
|
|
63660
|
+
* `eventMediaBaseUrl` above answers "is the plane up?", which is the right
|
|
63661
|
+
* question for the OPTIONAL `mediaUrl` stamp on a slim event projection.
|
|
63662
|
+
* It is the wrong question for a media row's `url`, which is that row's only
|
|
63663
|
+
* address: a row that dropped its address on a boot race would be
|
|
63664
|
+
* indistinguishable from a row with no bytes, and this repo has already paid
|
|
63665
|
+
* for one read that quietly returned less than it was asked for.
|
|
63666
|
+
*/
|
|
63667
|
+
eventMediaPathPrefix() {
|
|
63668
|
+
return `/addon/${this.ctx.id}/event-media`;
|
|
63669
|
+
}
|
|
63479
63670
|
lastActiveTrackIds = /* @__PURE__ */ new Map();
|
|
63480
63671
|
/** See `pipeline/suppressed-births.ts` — rejected births must not be
|
|
63481
63672
|
* re-upserted, and must be forgotten when the tracker drops the id. */
|
|
@@ -63726,6 +63917,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
63726
63917
|
sensorEventStore: () => this.sensorEventStore,
|
|
63727
63918
|
opsLog: () => this.eventsOpsLog,
|
|
63728
63919
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
63920
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
63729
63921
|
logger: () => this.ctx.logger,
|
|
63730
63922
|
zoneCatalogue: () => this.zoneCatalogue
|
|
63731
63923
|
});
|
|
@@ -66268,6 +66460,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66268
66460
|
logger: this.ctx.logger,
|
|
66269
66461
|
refreshGallery: () => this.faceRecognizer?.refreshGallery(),
|
|
66270
66462
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66463
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66271
66464
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload)
|
|
66272
66465
|
});
|
|
66273
66466
|
this.faceGalleryProvider = faceGallery;
|
|
@@ -66282,6 +66475,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66282
66475
|
this.plateRecognizer?.refreshGallery();
|
|
66283
66476
|
},
|
|
66284
66477
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66478
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66285
66479
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
|
|
66286
66480
|
});
|
|
66287
66481
|
return [
|
|
@@ -69967,6 +70161,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
69967
70161
|
async listTrackMedia(input) {
|
|
69968
70162
|
return this.queryFacade.listTrackMedia(input);
|
|
69969
70163
|
}
|
|
70164
|
+
/** What media an EVENT has, without any of it — the same manifest, for the
|
|
70165
|
+
* half of the detail modal's filmstrip that is event-owned. */
|
|
70166
|
+
async listEventMedia(input) {
|
|
70167
|
+
return this.queryFacade.listEventMedia(input);
|
|
70168
|
+
}
|
|
69970
70169
|
/**
|
|
69971
70170
|
* Search object events by text using CLIP cosine similarity.
|
|
69972
70171
|
*
|
|
@@ -71687,31 +71886,44 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71687
71886
|
return media;
|
|
71688
71887
|
}
|
|
71689
71888
|
}
|
|
71889
|
+
/**
|
|
71890
|
+
* Resolve an event/track id to ONE tile's bytes.
|
|
71891
|
+
*
|
|
71892
|
+
* Every ladder below ranks rows by `kind` alone, so this walks the INDEX
|
|
71893
|
+
* (`listInfoByOwner`, no blob opened) and opens only the winner. It used to
|
|
71894
|
+
* walk `listByOwner`, which reads and base64-encodes every blob an owner
|
|
71895
|
+
* holds before the first comparison is made: measured on the live hub
|
|
71896
|
+
* 2026-08-30, a track's six kinds are 3 597 219 bytes and the tile serves
|
|
71897
|
+
* `thumbnailSmall` — 31 155 of them. An Events page of forty tiles read
|
|
71898
|
+
* ~144 MB and allocated ~190 MB of base64 to deliver ~1.2 MB, all of it on
|
|
71899
|
+
* the single-threaded post-analysis runner.
|
|
71900
|
+
*/
|
|
71690
71901
|
async readEventThumbnail(id, preferKind) {
|
|
71691
|
-
const
|
|
71902
|
+
const listInfo = (ownerKind, ownerId) => this.mediaStore?.listInfoByOwner(ownerKind, ownerId) ?? Promise.resolve([]);
|
|
71903
|
+
const getTrackIdForEvent = (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null);
|
|
71904
|
+
const readBlob = async (key) => {
|
|
71905
|
+
const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
|
|
71906
|
+
return file === null ? null : {
|
|
71907
|
+
bytes: Buffer.from(file.base64, "base64"),
|
|
71908
|
+
key: file.key
|
|
71909
|
+
};
|
|
71910
|
+
};
|
|
71911
|
+
const eventFiles = await listInfo("event", id);
|
|
71692
71912
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
71693
71913
|
const layers = await collectCropForcedMedia({
|
|
71694
|
-
listByOwner:
|
|
71695
|
-
getTrackIdForEvent
|
|
71914
|
+
listByOwner: listInfo,
|
|
71915
|
+
getTrackIdForEvent
|
|
71696
71916
|
}, id);
|
|
71697
|
-
const
|
|
71698
|
-
|
|
71699
|
-
return {
|
|
71700
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71701
|
-
key: chosen.key
|
|
71702
|
-
};
|
|
71917
|
+
const bodyKeys = new Set(layers.bodyFiles.map((f) => f.key));
|
|
71918
|
+
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);
|
|
71703
71919
|
}
|
|
71704
|
-
const
|
|
71920
|
+
const { candidates, pick } = await collectDefaultEventMedia({
|
|
71705
71921
|
eventId: id,
|
|
71706
71922
|
eventFiles,
|
|
71707
|
-
listTrackMedia: (trackId) =>
|
|
71708
|
-
getTrackIdForEvent
|
|
71923
|
+
listTrackMedia: (trackId) => listInfo("track", trackId),
|
|
71924
|
+
getTrackIdForEvent
|
|
71709
71925
|
});
|
|
71710
|
-
|
|
71711
|
-
return {
|
|
71712
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71713
|
-
key: chosen.key
|
|
71714
|
-
};
|
|
71926
|
+
return await readWinningBlob(candidates, pick, readBlob);
|
|
71715
71927
|
}
|
|
71716
71928
|
globalSettingsSchema(cap) {
|
|
71717
71929
|
return this.schema(buildGlobalSettingsSchema(cap));
|