@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
|
@@ -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.
|
|
@@ -6263,12 +6263,107 @@ var NcDeviceStateCache = class {
|
|
|
6263
6263
|
}
|
|
6264
6264
|
};
|
|
6265
6265
|
//#endregion
|
|
6266
|
+
//#region src/notification-center/system-event-tap-url.ts
|
|
6267
|
+
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
6268
|
+
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
6269
|
+
var ADMIN_CLUSTER_PATH = "/system/cluster";
|
|
6270
|
+
var ADMIN_DEVICES_PATH = "/devices";
|
|
6271
|
+
/**
|
|
6272
|
+
* The query parameter naming a node.
|
|
6273
|
+
*
|
|
6274
|
+
* One spelling for both admin pages, mirrored by `NODE_PARAM` in
|
|
6275
|
+
* `packages/addon-admin-ui/src/lib/node-url-selection.ts` and pinned across
|
|
6276
|
+
* the two packages by `scripts/check-notification-tap-targets.ts`.
|
|
6277
|
+
*/
|
|
6278
|
+
var NODE_PARAM = "node";
|
|
6279
|
+
/** The device page's top-level tab parameter, read by `DeviceDetail`. */
|
|
6280
|
+
var TAB_PARAM = "tab";
|
|
6281
|
+
/** The Stream Broker tab id — a `WELL_KNOWN_TABS` member, hoisted `top-tab`. */
|
|
6282
|
+
var STREAM_BROKER_TAB = "stream-broker";
|
|
6283
|
+
/** `DeviceType.Camera`. Spelled here rather than imported so this stays a pure
|
|
6284
|
+
* string map with no device-model dependency; the guard pins it. */
|
|
6285
|
+
var CAMERA_DEVICE_TYPE = "camera";
|
|
6286
|
+
/** Kinds whose action is "go and apply an ADDON update". */
|
|
6287
|
+
var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
6288
|
+
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
6289
|
+
var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
|
|
6290
|
+
/**
|
|
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.
|
|
6301
|
+
*
|
|
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
|
|
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.
|
|
6309
|
+
*/
|
|
6310
|
+
var DEVICE_STREAM_KINDS = new Set([
|
|
6311
|
+
"device-offline",
|
|
6312
|
+
"device-online",
|
|
6313
|
+
"stream-offline",
|
|
6314
|
+
"stream-online",
|
|
6315
|
+
"camera-offline",
|
|
6316
|
+
"camera-online"
|
|
6317
|
+
]);
|
|
6318
|
+
/**
|
|
6319
|
+
* Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
|
|
6320
|
+
*
|
|
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.
|
|
6324
|
+
*/
|
|
6325
|
+
var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
|
|
6326
|
+
"stream-offline",
|
|
6327
|
+
"stream-online",
|
|
6328
|
+
"camera-offline",
|
|
6329
|
+
"camera-online"
|
|
6330
|
+
]);
|
|
6331
|
+
function withParam(path, key, value) {
|
|
6332
|
+
if (value === void 0 || value.length === 0) return path;
|
|
6333
|
+
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
6334
|
+
}
|
|
6335
|
+
/**
|
|
6336
|
+
* The admin path (with its query) a system event taps through to, or `null`
|
|
6337
|
+
* when the kind has no action.
|
|
6338
|
+
*
|
|
6339
|
+
* Exported for the cross-package guard and for tests that want the path
|
|
6340
|
+
* without a base; the dispatcher goes through {@link systemEventTapUrl}.
|
|
6341
|
+
*/
|
|
6342
|
+
function systemEventTapPath(subject) {
|
|
6343
|
+
if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
|
|
6344
|
+
if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
|
|
6345
|
+
if (DEVICE_STREAM_KINDS.has(subject.kind)) {
|
|
6346
|
+
const deviceId = subject.deviceId;
|
|
6347
|
+
if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
6348
|
+
const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
|
|
6349
|
+
return CAMERA_BY_CONSTRUCTION_KINDS.has(subject.kind) || subject.deviceType === CAMERA_DEVICE_TYPE ? withParam(devicePath, TAB_PARAM, STREAM_BROKER_TAB) : devicePath;
|
|
6350
|
+
}
|
|
6351
|
+
return null;
|
|
6352
|
+
}
|
|
6353
|
+
/** Full URL, or `null` when the kind has no action or no base is reachable. */
|
|
6354
|
+
function systemEventTapUrl(subject, baseUrl) {
|
|
6355
|
+
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
6356
|
+
const path = systemEventTapPath(subject);
|
|
6357
|
+
if (path === null) return null;
|
|
6358
|
+
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
6359
|
+
}
|
|
6360
|
+
//#endregion
|
|
6266
6361
|
//#region src/notification-center/default-tap-url.ts
|
|
6267
6362
|
/** The viewer's hosted base path under the hub. */
|
|
6268
6363
|
var VIEWER_BASE_PATH = "/viewer/camstack";
|
|
6269
6364
|
function defaultTapUrl(input) {
|
|
6270
|
-
if (input.systemEvent === true) return null;
|
|
6271
6365
|
if (input.baseUrl === null || input.baseUrl.length === 0) return null;
|
|
6366
|
+
if (input.systemEvent !== void 0) return systemEventTapUrl(input.systemEvent, input.baseUrl);
|
|
6272
6367
|
if (!Number.isInteger(input.deviceId) || input.deviceId <= 0) return null;
|
|
6273
6368
|
return `${input.baseUrl.replace(/\/+$/, "")}${VIEWER_BASE_PATH}/camera/${String(input.deviceId)}?at=${String(input.timestampMs)}`;
|
|
6274
6369
|
}
|
|
@@ -8861,13 +8956,14 @@ function trailAt(trail, timestamp) {
|
|
|
8861
8956
|
* cannot be asked (no trail, no polygon) or when nobody qualifies.
|
|
8862
8957
|
*/
|
|
8863
8958
|
function selectTriggerZoneCandidates(input) {
|
|
8864
|
-
const { candidates, trail, polygons } = input;
|
|
8959
|
+
const { candidates, trail, polygons, strict } = input;
|
|
8865
8960
|
if (candidates.length === 0 || trail.length === 0 || polygons.length === 0) return candidates;
|
|
8866
8961
|
const qualifying = candidates.filter((candidate) => {
|
|
8867
8962
|
const point = trailAt(trail, candidate.timestamp);
|
|
8868
8963
|
return point !== null && zoneOverlap(point, polygons) > 0;
|
|
8869
8964
|
});
|
|
8870
|
-
|
|
8965
|
+
if (qualifying.length > 0) return qualifying;
|
|
8966
|
+
return strict === true ? [] : candidates;
|
|
8871
8967
|
}
|
|
8872
8968
|
//#endregion
|
|
8873
8969
|
//#region src/notification-center/dispatcher.ts
|
|
@@ -8905,6 +9001,24 @@ var NC_STILL_WAIT_MS = 3e3;
|
|
|
8905
9001
|
* is the instant; the ladder then runs among the frames that are about it.
|
|
8906
9002
|
*/
|
|
8907
9003
|
var NC_TRIGGER_STILL_MAX_AGE_MS = TRAIL_MATCH_MAX_MS;
|
|
9004
|
+
/**
|
|
9005
|
+
* Whether this row's subject can still be photographed AGAIN after its trigger.
|
|
9006
|
+
*
|
|
9007
|
+
* The one thing that separates a row worth waiting for from a row whose only
|
|
9008
|
+
* frame already exists. A live visual track keeps writing media for as long as
|
|
9009
|
+
* it is open — its first frame, its periodic snapshots, its last frame all land
|
|
9010
|
+
* after the instant a zone condition fired. Every other record is photographed
|
|
9011
|
+
* once: an audio window, a device event and a package event are served by the
|
|
9012
|
+
* still shelf, which D178 makes take ONE picture for a whole burst, and a
|
|
9013
|
+
* `track-end` row is by definition past the last of them.
|
|
9014
|
+
*
|
|
9015
|
+
* Waiting on the wrong side of this line has cost both ways in production — 3 s
|
|
9016
|
+
* of latency on a scream when nothing more was coming, and an empty doorstep on
|
|
9017
|
+
* 'Persona su Uscio' when something was.
|
|
9018
|
+
*/
|
|
9019
|
+
function canStillGainMedia(entry) {
|
|
9020
|
+
return entry.recordKind === "object-event";
|
|
9021
|
+
}
|
|
8908
9022
|
var NcDispatcher = class {
|
|
8909
9023
|
deps;
|
|
8910
9024
|
targetCache = null;
|
|
@@ -9384,7 +9498,12 @@ var NcDispatcher = class {
|
|
|
9384
9498
|
deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
|
|
9385
9499
|
timestampMs: subject.timestamp,
|
|
9386
9500
|
baseUrl: await this.deps.viewerUrlBase().catch(() => null),
|
|
9387
|
-
systemEvent
|
|
9501
|
+
...systemEvent !== void 0 ? { systemEvent: {
|
|
9502
|
+
kind: systemEvent.kind,
|
|
9503
|
+
...systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {},
|
|
9504
|
+
...systemEvent.deviceType !== void 0 ? { deviceType: systemEvent.deviceType } : {},
|
|
9505
|
+
...systemEvent.nodeId !== void 0 ? { nodeId: systemEvent.nodeId } : {}
|
|
9506
|
+
} } : {}
|
|
9388
9507
|
}) : null;
|
|
9389
9508
|
const actions = await this.mintActions(entry, target);
|
|
9390
9509
|
return {
|
|
@@ -9519,7 +9638,7 @@ var NcDispatcher = class {
|
|
|
9519
9638
|
const out = [];
|
|
9520
9639
|
const zoneIdsWanted = entry.payload.mediaZoneIds;
|
|
9521
9640
|
const stillOverride = zoneIdsWanted !== void 0 && zoneIdsWanted.length > 0 ? "keyFrame" : void 0;
|
|
9522
|
-
const firstLook = await this.resolveAttachment(entry, stillOverride, "any");
|
|
9641
|
+
const firstLook = await this.resolveAttachment(entry, stillOverride, canStillGainMedia(entry) ? "trigger-only" : "any");
|
|
9523
9642
|
const footagePending = this.renderFootage(entry);
|
|
9524
9643
|
const still = firstLook !== null || entry.payload.media === "none" ? firstLook : await this.waitForStill(entry, stillOverride);
|
|
9525
9644
|
const footage = await footagePending;
|
|
@@ -9763,20 +9882,34 @@ var NcDispatcher = class {
|
|
|
9763
9882
|
* throw. And when nothing qualifies, the full list comes back: a real picture
|
|
9764
9883
|
* beats no picture (the same rule D212 settled for the best shot).
|
|
9765
9884
|
*/
|
|
9766
|
-
async narrowToTriggerZone(entry, ordered) {
|
|
9885
|
+
async narrowToTriggerZone(entry, ordered, freshness) {
|
|
9767
9886
|
const zoneIds = entry.payload.triggerZoneIds;
|
|
9768
9887
|
const trackId = entry.payload.subject.trackId;
|
|
9769
9888
|
const getTrail = this.deps.getSubjectTrail;
|
|
9770
9889
|
const deviceId = entry.payload.subject.deviceId;
|
|
9771
|
-
|
|
9890
|
+
const strict = freshness === "trigger-only";
|
|
9891
|
+
if (zoneIds === void 0 || zoneIds.length === 0 || trackId === void 0 || getTrail === void 0 || ordered.length < 2 && !strict) return ordered;
|
|
9772
9892
|
try {
|
|
9773
9893
|
const [polygons, trail] = await Promise.all([this.deps.getZonePolygons?.(deviceId, zoneIds) ?? Promise.resolve([]), getTrail(deviceId, trackId)]);
|
|
9774
9894
|
const kept = selectTriggerZoneCandidates({
|
|
9775
9895
|
candidates: ordered,
|
|
9776
9896
|
trail,
|
|
9777
9897
|
polygons,
|
|
9778
|
-
triggerAt: entry.payload.subject.timestamp
|
|
9898
|
+
triggerAt: entry.payload.subject.timestamp,
|
|
9899
|
+
strict
|
|
9779
9900
|
});
|
|
9901
|
+
if (kept.length === 0) {
|
|
9902
|
+
this.deps.logger.debug("no still from the zone that fired the rule — still waiting", {
|
|
9903
|
+
tags: { deviceId },
|
|
9904
|
+
meta: {
|
|
9905
|
+
ruleId: entry.ruleId,
|
|
9906
|
+
trackId,
|
|
9907
|
+
zoneIds: zoneIds.join(","),
|
|
9908
|
+
rejected: ordered.length
|
|
9909
|
+
}
|
|
9910
|
+
});
|
|
9911
|
+
return kept;
|
|
9912
|
+
}
|
|
9780
9913
|
if (kept.length === ordered.length) return kept;
|
|
9781
9914
|
this.deps.logger.info("still narrowed to the zone that fired the rule", {
|
|
9782
9915
|
tags: { deviceId },
|
|
@@ -9856,7 +9989,8 @@ var NcDispatcher = class {
|
|
|
9856
9989
|
for (const kind of preference) for (const file of files) if (file.kind === kind) ordered.push(file);
|
|
9857
9990
|
const inBand = this.narrowToTriggerInstant(entry, ordered, freshness);
|
|
9858
9991
|
if (inBand.length === 0) continue;
|
|
9859
|
-
const shortlist = await this.narrowToTriggerZone(entry, inBand);
|
|
9992
|
+
const shortlist = await this.narrowToTriggerZone(entry, inBand, freshness);
|
|
9993
|
+
if (shortlist.length === 0) continue;
|
|
9860
9994
|
for (const file of shortlist) {
|
|
9861
9995
|
const raw = Buffer.from(file.base64, "base64");
|
|
9862
9996
|
if (raw.byteLength === 0) continue;
|
|
@@ -10610,16 +10744,26 @@ function incomingFromUpdateAvailable(facts, data) {
|
|
|
10610
10744
|
...nodeIds.length > 0 ? { nodeIds } : {}
|
|
10611
10745
|
});
|
|
10612
10746
|
}
|
|
10613
|
-
/**
|
|
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
|
+
*/
|
|
10614
10758
|
function parseAddonUpdateComplete(data) {
|
|
10615
10759
|
const packageName = stringField(data, "packageName");
|
|
10616
10760
|
const fromVersion = stringField(data, "fromVersion");
|
|
10617
10761
|
const toVersion = stringField(data, "toVersion");
|
|
10618
|
-
if (packageName === void 0 ||
|
|
10762
|
+
if (packageName === void 0 || toVersion === void 0) return null;
|
|
10619
10763
|
const nodeId = stringField(data, "agent") ?? stringField(data, "nodeId");
|
|
10620
10764
|
return {
|
|
10621
10765
|
packageName,
|
|
10622
|
-
fromVersion,
|
|
10766
|
+
...fromVersion !== void 0 ? { fromVersion } : {},
|
|
10623
10767
|
toVersion,
|
|
10624
10768
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10625
10769
|
};
|
|
@@ -10629,7 +10773,10 @@ function incomingFromAddonUpdates(facts, updates) {
|
|
|
10629
10773
|
const packageNames = unique(updates.map((update) => update.packageName));
|
|
10630
10774
|
const nodeIds = unique(updates.map((update) => update.nodeId).filter((id) => id !== void 0));
|
|
10631
10775
|
const signature = updates.map((update) => `${update.nodeId ?? ""}:${update.packageName}@${update.toVersion}`).toSorted().join(",");
|
|
10632
|
-
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");
|
|
10633
10780
|
const head = updates[0];
|
|
10634
10781
|
return systemSubject({
|
|
10635
10782
|
facts,
|
|
@@ -23451,17 +23598,17 @@ function planRuleTest(input) {
|
|
|
23451
23598
|
deviceName: `camera ${String(systemScope.deviceId ?? deviceId)}`,
|
|
23452
23599
|
nodeId: systemScope.nodeId ?? "hub",
|
|
23453
23600
|
packageName: systemScope.packageName ?? "@camstack/server",
|
|
23454
|
-
currentVersion: "
|
|
23455
|
-
latestVersion: "
|
|
23601
|
+
currentVersion: "1.0.0",
|
|
23602
|
+
latestVersion: "1.0.1",
|
|
23456
23603
|
profile: "low",
|
|
23457
23604
|
seconds: "30",
|
|
23458
23605
|
devices: "cuda:0",
|
|
23459
23606
|
minutes: "15",
|
|
23460
23607
|
addons: TEST_CRASHED_RUNNER,
|
|
23461
|
-
list: "@camstack/addon-pipeline 1.0.
|
|
23608
|
+
list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
|
|
23462
23609
|
nodes: "hub",
|
|
23463
|
-
fromVersion: "
|
|
23464
|
-
toVersion: "
|
|
23610
|
+
fromVersion: "1.0.0",
|
|
23611
|
+
toVersion: "1.0.1",
|
|
23465
23612
|
title: "Export",
|
|
23466
23613
|
count: "1"
|
|
23467
23614
|
},
|
|
@@ -25487,13 +25634,46 @@ var MEDIA_COLUMNS = [
|
|
|
25487
25634
|
type: "TEXT"
|
|
25488
25635
|
})
|
|
25489
25636
|
];
|
|
25490
|
-
var MEDIA_INDEXES = [
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25495
|
-
|
|
25496
|
-
|
|
25637
|
+
var MEDIA_INDEXES = [
|
|
25638
|
+
{
|
|
25639
|
+
name: "idx_media_owner",
|
|
25640
|
+
columns: ["ownerKind", "ownerId"]
|
|
25641
|
+
},
|
|
25642
|
+
{
|
|
25643
|
+
name: "idx_media_device_ts",
|
|
25644
|
+
columns: ["deviceId", "timestamp"]
|
|
25645
|
+
},
|
|
25646
|
+
(
|
|
25647
|
+
/**
|
|
25648
|
+
* The `locationId IS NULL` seal gate — see `media-relocate-engine.ts`.
|
|
25649
|
+
*
|
|
25650
|
+
* NULLs sort FIRST in a SQLite index, so `WHERE locationId IS NULL` is a
|
|
25651
|
+
* contiguous range from the head of the b-tree: its cost is proportional to
|
|
25652
|
+
* the ANSWER, not to the table. That is the property the gate needs. The
|
|
25653
|
+
* question it must answer fastest is "are there none left", and with this
|
|
25654
|
+
* index that is one seek that immediately falls off the end of an empty
|
|
25655
|
+
* range — regardless of how many million stamped rows sit behind it.
|
|
25656
|
+
*
|
|
25657
|
+
* **What it costs.** One extra b-tree maintained by every insert into a
|
|
25658
|
+
* table taking ~92 k rows/day, and roughly 25 bytes of index per row (~30 MB
|
|
25659
|
+
* at 1.25 M rows). At ~1 insert/s sustained that maintenance is noise
|
|
25660
|
+
* against the JPEG write the same call performs. The one-off cost is real
|
|
25661
|
+
* and worth naming: the FIRST declare after this ships builds the index over
|
|
25662
|
+
* every existing row, on a recordings volume that is currently saturated —
|
|
25663
|
+
* expect the analytics addon's boot to stall for tens of seconds to a couple
|
|
25664
|
+
* of minutes, once.
|
|
25665
|
+
*
|
|
25666
|
+
* A PARTIAL index (`WHERE locationId IS NULL`) would be smaller still and
|
|
25667
|
+
* would cost nothing at all once sealed. It is not used because
|
|
25668
|
+
* `CollectionIndexSchema` has no predicate and the schema fingerprint does
|
|
25669
|
+
* not hash one — adding both to serve one query is a bigger change than the
|
|
25670
|
+
* query deserves, and the plain index already makes the gate O(log n).
|
|
25671
|
+
*/
|
|
25672
|
+
{
|
|
25673
|
+
name: "idx_media_location",
|
|
25674
|
+
columns: ["locationId"]
|
|
25675
|
+
})
|
|
25676
|
+
];
|
|
25497
25677
|
/** The storage location of a media row/record: its stamped `locationId`, or
|
|
25498
25678
|
* the default `eventMedia` location for NULL (pre-multi-location) rows. */
|
|
25499
25679
|
var DEFAULT_MEDIA_LOCATION = "eventMedia";
|
|
@@ -25578,7 +25758,7 @@ var MediaStore = class {
|
|
|
25578
25758
|
});
|
|
25579
25759
|
}
|
|
25580
25760
|
/** Persist a media blob and register its metadata row. Returns the
|
|
25581
|
-
* collection key (
|
|
25761
|
+
* collection key (MediaFileRef.key) that callers store as reference. */
|
|
25582
25762
|
async put(params) {
|
|
25583
25763
|
if (this.writeGate) return this.writeGate.run(() => this.putUnchecked(params));
|
|
25584
25764
|
return this.putUnchecked(params);
|
|
@@ -25900,8 +26080,20 @@ var MediaStore = class {
|
|
|
25900
26080
|
* its device at write time, so scoping the query by it makes a media read
|
|
25901
26081
|
* for a device the caller cannot name return nothing — the DB itself
|
|
25902
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.
|
|
25903
26095
|
*/
|
|
25904
|
-
async listByOwner(ownerKind, ownerId, deviceId) {
|
|
26096
|
+
async listByOwner(ownerKind, ownerId, deviceId, kinds) {
|
|
25905
26097
|
const rows = await this.store.query.query({
|
|
25906
26098
|
collection: MEDIA_COLLECTION,
|
|
25907
26099
|
filter: {
|
|
@@ -25913,6 +26105,7 @@ var MediaStore = class {
|
|
|
25913
26105
|
ownerId,
|
|
25914
26106
|
deviceId
|
|
25915
26107
|
},
|
|
26108
|
+
...kinds !== void 0 && kinds.length > 0 ? { whereIn: { kind: [...kinds] } } : {},
|
|
25916
26109
|
orderBy: {
|
|
25917
26110
|
field: "timestamp",
|
|
25918
26111
|
direction: "asc"
|
|
@@ -27828,6 +28021,63 @@ var embeddingActions = defineCustomActions({
|
|
|
27828
28021
|
"embedding.reembedFacesStatus": customAction(NoInputSchema$1, ReembedFacesStatusSchema)
|
|
27829
28022
|
});
|
|
27830
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
|
|
27831
28081
|
//#region src/pipeline-analytics/include-crops-default.ts
|
|
27832
28082
|
function resolveIncludeCrops(input) {
|
|
27833
28083
|
return input?.includeCrops === true;
|
|
@@ -29487,6 +29737,7 @@ var FaceGalleryProvider = class {
|
|
|
29487
29737
|
logger;
|
|
29488
29738
|
refreshGallery;
|
|
29489
29739
|
eventMediaBaseUrl;
|
|
29740
|
+
eventMediaPathPrefix;
|
|
29490
29741
|
emitFaceGalleryChanged;
|
|
29491
29742
|
constructor(deps) {
|
|
29492
29743
|
this.identityStore = deps.identityStore;
|
|
@@ -29497,6 +29748,7 @@ var FaceGalleryProvider = class {
|
|
|
29497
29748
|
this.logger = deps.logger;
|
|
29498
29749
|
this.refreshGallery = deps.refreshGallery;
|
|
29499
29750
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
29751
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
29500
29752
|
this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
|
|
29501
29753
|
}
|
|
29502
29754
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
@@ -29684,13 +29936,7 @@ var FaceGalleryProvider = class {
|
|
|
29684
29936
|
* the enrolled identity copies — those live under identity owner).
|
|
29685
29937
|
*/
|
|
29686
29938
|
async getFaceMedia(input) {
|
|
29687
|
-
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => (
|
|
29688
|
-
key: m.key,
|
|
29689
|
-
kind: m.kind,
|
|
29690
|
-
base64: m.base64,
|
|
29691
|
-
sizeBytes: m.sizeBytes,
|
|
29692
|
-
timestamp: m.timestamp
|
|
29693
|
-
}));
|
|
29939
|
+
return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
29694
29940
|
}
|
|
29695
29941
|
/**
|
|
29696
29942
|
* Assign a buffered face to an identity:
|
|
@@ -32401,209 +32647,6 @@ var FrameProcessor = class {
|
|
|
32401
32647
|
};
|
|
32402
32648
|
}
|
|
32403
32649
|
};
|
|
32404
|
-
//#endregion
|
|
32405
|
-
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
32406
|
-
/**
|
|
32407
|
-
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
32408
|
-
* return the tracks it produced.
|
|
32409
|
-
*
|
|
32410
|
-
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
32411
|
-
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
32412
|
-
*/
|
|
32413
|
-
function runFrameProcessorPass(input) {
|
|
32414
|
-
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
32415
|
-
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
32416
|
-
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
32417
|
-
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
32418
|
-
const accum = /* @__PURE__ */ new Map();
|
|
32419
|
-
for (const { timestamp, frame } of input.frames) {
|
|
32420
|
-
const result = processor.process({
|
|
32421
|
-
timestamp,
|
|
32422
|
-
frame
|
|
32423
|
-
});
|
|
32424
|
-
for (const td of result.tracked) {
|
|
32425
|
-
const existing = accum.get(td.trackId);
|
|
32426
|
-
const matched = td.matchedThisFrame ? 1 : 0;
|
|
32427
|
-
if (existing === void 0) accum.set(td.trackId, {
|
|
32428
|
-
className: td.className,
|
|
32429
|
-
firstSeenMs: timestamp,
|
|
32430
|
-
lastSeenMs: timestamp,
|
|
32431
|
-
bbox: { ...td.bbox },
|
|
32432
|
-
framesMatched: matched
|
|
32433
|
-
});
|
|
32434
|
-
else {
|
|
32435
|
-
existing.className = td.className;
|
|
32436
|
-
existing.lastSeenMs = timestamp;
|
|
32437
|
-
existing.framesMatched += matched;
|
|
32438
|
-
}
|
|
32439
|
-
}
|
|
32440
|
-
}
|
|
32441
|
-
return { tracks: [...accum.values()].map((t) => ({
|
|
32442
|
-
className: t.className,
|
|
32443
|
-
firstSeenMs: t.firstSeenMs,
|
|
32444
|
-
lastSeenMs: t.lastSeenMs,
|
|
32445
|
-
bbox: t.bbox,
|
|
32446
|
-
framesMatched: t.framesMatched
|
|
32447
|
-
})) };
|
|
32448
|
-
}
|
|
32449
|
-
function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, score) {
|
|
32450
|
-
if (frameWidth <= 0 || frameHeight <= 0) return true;
|
|
32451
|
-
if (score !== void 0 && score >= .5) return true;
|
|
32452
|
-
if (bbox.w * bbox.h / (frameWidth * frameHeight) >= .85) return false;
|
|
32453
|
-
if (bbox.h >= frameHeight * .95) return false;
|
|
32454
|
-
if (bbox.w >= frameWidth * .95) return false;
|
|
32455
|
-
return true;
|
|
32456
|
-
}
|
|
32457
|
-
/**
|
|
32458
|
-
* Minimum fraction of the track's BEST-SEEN bbox area a retry frame's bbox must
|
|
32459
|
-
* still cover to be an acceptable best-`thumbnail` RETRY subject. 0.45 tolerates
|
|
32460
|
-
* normal breathing of the tracker box (~2/3 per side) while rejecting the
|
|
32461
|
-
* degenerate shrink (observed live 2026-07-22: a car born at 78px retried at
|
|
32462
|
-
* 26px once the native lease warmed — a ~0.11 area fraction — and the landed
|
|
32463
|
-
* "best" thumbnail was a sliver of a distant car).
|
|
32464
|
-
*/
|
|
32465
|
-
var RETRY_MIN_FRACTION_OF_BEST = .45;
|
|
32466
|
-
/**
|
|
32467
|
-
* RETRY-ONLY gate (#27-A degenerate-retry fix): on a cold on-motion session the
|
|
32468
|
-
* native lease misses the first seconds, so the large-subject captures return
|
|
32469
|
-
* null and the per-frame retry re-fires with the CURRENT frame's bbox. By the
|
|
32470
|
-
* time the lease warms the subject may have shrunk to a sliver; capturing THAT
|
|
32471
|
-
* frame lands a genuine-native-but-useless thumbnail and `thumbnailLanded`
|
|
32472
|
-
* stops all further retries. Reject a retry whose current bbox area has
|
|
32473
|
-
* collapsed below `minFractionOfBest` of the track's best-seen bbox area.
|
|
32474
|
-
*
|
|
32475
|
-
* Applies ONLY to the `!thumbnailLanded` retry path — a genuine new-best
|
|
32476
|
-
* capture is never routed through this gate (the caller bypasses it on
|
|
32477
|
-
* `isNewBest`). With no best-seen reference (or a degenerate one) there is no
|
|
32478
|
-
* info to reject on → plausible.
|
|
32479
|
-
*/
|
|
32480
|
-
function isPlausibleRetryBox(current, bestSeen, minFractionOfBest = RETRY_MIN_FRACTION_OF_BEST) {
|
|
32481
|
-
if (bestSeen === null) return true;
|
|
32482
|
-
const bestArea = bestSeen.w * bestSeen.h;
|
|
32483
|
-
if (bestArea <= 0) return true;
|
|
32484
|
-
return current.w * current.h >= minFractionOfBest * bestArea;
|
|
32485
|
-
}
|
|
32486
|
-
//#endregion
|
|
32487
|
-
//#region src/pipeline-analytics/pipeline/semantic-promotion.ts
|
|
32488
|
-
/**
|
|
32489
|
-
* Compose the {@link SemanticRecognitionFrame} for a plate read / accepted
|
|
32490
|
-
* fine classification. ONE rule, enforced here rather than at the call site:
|
|
32491
|
-
* the promoted `bbox` is ALWAYS the SUBJECT box (`frame.bbox`), never the
|
|
32492
|
-
* detail box — see {@link DetailAttributionResult.bbox} for what feeding the
|
|
32493
|
-
* detail box cost. The face path (`face-recognizer.ts` `onSemanticMatch`)
|
|
32494
|
-
* already passes its body box; this keeps the plate/classification path on the
|
|
32495
|
-
* same discipline.
|
|
32496
|
-
*/
|
|
32497
|
-
function semanticFrameForDetailAttribution(deviceId, trackId, detail, frame) {
|
|
32498
|
-
return {
|
|
32499
|
-
deviceId,
|
|
32500
|
-
trackId,
|
|
32501
|
-
timestamp: frame.timestamp,
|
|
32502
|
-
bbox: {
|
|
32503
|
-
x: frame.bbox.x,
|
|
32504
|
-
y: frame.bbox.y,
|
|
32505
|
-
w: frame.bbox.w,
|
|
32506
|
-
h: frame.bbox.h
|
|
32507
|
-
},
|
|
32508
|
-
frameWidth: frame.frameWidth,
|
|
32509
|
-
frameHeight: frame.frameHeight,
|
|
32510
|
-
kind: detail.className === "plate" ? "plate" : "classification",
|
|
32511
|
-
score: detail.score,
|
|
32512
|
-
confidence: frame.confidence,
|
|
32513
|
-
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
32514
|
-
};
|
|
32515
|
-
}
|
|
32516
|
-
/**
|
|
32517
|
-
* Offer the recognition frame to the best-frame ranking with the semantic
|
|
32518
|
-
* tier. On a win: record the best-seen bbox (plausible boxes only — an
|
|
32519
|
-
* envelope-exploded box must not poison the retry reference, mirroring
|
|
32520
|
-
* `buildSnapshotTargets`) and re-fire the captures when the pixels are still
|
|
32521
|
-
* reachable.
|
|
32522
|
-
*/
|
|
32523
|
-
function promoteSemanticBestFrame(deps, input) {
|
|
32524
|
-
const { deviceId, trackId, bbox, frameWidth, frameHeight, timestamp } = input;
|
|
32525
|
-
if (!deps.observeBestFrame({
|
|
32526
|
-
deviceId,
|
|
32527
|
-
trackId,
|
|
32528
|
-
confidence: input.confidence,
|
|
32529
|
-
timestamp,
|
|
32530
|
-
edgeClear: isEdgeClear({
|
|
32531
|
-
bbox,
|
|
32532
|
-
frameWidth,
|
|
32533
|
-
frameHeight
|
|
32534
|
-
}),
|
|
32535
|
-
centerScore: bboxCenterScore(bbox, frameWidth, frameHeight),
|
|
32536
|
-
containment: bboxContainment(bbox, frameWidth, frameHeight),
|
|
32537
|
-
sizeScore: bboxSizeScore(bbox, frameWidth, frameHeight),
|
|
32538
|
-
squareFraction: centralSquareContainmentFraction(bbox, {
|
|
32539
|
-
W: frameWidth,
|
|
32540
|
-
H: frameHeight
|
|
32541
|
-
}),
|
|
32542
|
-
semantic: true
|
|
32543
|
-
})) {
|
|
32544
|
-
deps.logger.debug("best-shot: semantic frame did not beat the held peak", {
|
|
32545
|
-
tags: { deviceId },
|
|
32546
|
-
meta: {
|
|
32547
|
-
trackId,
|
|
32548
|
-
kind: input.kind,
|
|
32549
|
-
score: input.score,
|
|
32550
|
-
timestamp
|
|
32551
|
-
}
|
|
32552
|
-
});
|
|
32553
|
-
return {
|
|
32554
|
-
promoted: false,
|
|
32555
|
-
captured: false
|
|
32556
|
-
};
|
|
32557
|
-
}
|
|
32558
|
-
const plausible = isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, input.confidence);
|
|
32559
|
-
if (plausible) deps.recordBestSeenBbox(deviceId, trackId, {
|
|
32560
|
-
x: bbox.x,
|
|
32561
|
-
y: bbox.y,
|
|
32562
|
-
w: bbox.w,
|
|
32563
|
-
h: bbox.h,
|
|
32564
|
-
frameWidth,
|
|
32565
|
-
frameHeight,
|
|
32566
|
-
timestamp
|
|
32567
|
-
});
|
|
32568
|
-
if (!plausible || input.frameHandle === void 0) {
|
|
32569
|
-
deps.logger.info("best-shot: semantic promotion without capture", {
|
|
32570
|
-
tags: { deviceId },
|
|
32571
|
-
meta: {
|
|
32572
|
-
trackId,
|
|
32573
|
-
kind: input.kind,
|
|
32574
|
-
score: input.score,
|
|
32575
|
-
timestamp,
|
|
32576
|
-
reason: plausible ? "no frame handle" : "implausible box"
|
|
32577
|
-
}
|
|
32578
|
-
});
|
|
32579
|
-
return {
|
|
32580
|
-
promoted: true,
|
|
32581
|
-
captured: false
|
|
32582
|
-
};
|
|
32583
|
-
}
|
|
32584
|
-
deps.captureBestShot({
|
|
32585
|
-
deviceId,
|
|
32586
|
-
trackId,
|
|
32587
|
-
timestamp,
|
|
32588
|
-
bbox,
|
|
32589
|
-
frameWidth,
|
|
32590
|
-
frameHeight,
|
|
32591
|
-
frameHandle: input.frameHandle
|
|
32592
|
-
});
|
|
32593
|
-
deps.logger.info("best-shot: semantic promotion", {
|
|
32594
|
-
tags: { deviceId },
|
|
32595
|
-
meta: {
|
|
32596
|
-
trackId,
|
|
32597
|
-
kind: input.kind,
|
|
32598
|
-
score: input.score,
|
|
32599
|
-
timestamp
|
|
32600
|
-
}
|
|
32601
|
-
});
|
|
32602
|
-
return {
|
|
32603
|
-
promoted: true,
|
|
32604
|
-
captured: true
|
|
32605
|
-
};
|
|
32606
|
-
}
|
|
32607
32650
|
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
32608
32651
|
* evicts the oldest record. */
|
|
32609
32652
|
var PendingPreviewUpgrades = class {
|
|
@@ -32735,6 +32778,164 @@ async function runPreviewUpgrade(deps, input) {
|
|
|
32735
32778
|
});
|
|
32736
32779
|
return "upgraded";
|
|
32737
32780
|
}
|
|
32781
|
+
function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, score) {
|
|
32782
|
+
if (frameWidth <= 0 || frameHeight <= 0) return true;
|
|
32783
|
+
if (score !== void 0 && score >= .5) return true;
|
|
32784
|
+
if (bbox.w * bbox.h / (frameWidth * frameHeight) >= .85) return false;
|
|
32785
|
+
if (bbox.h >= frameHeight * .95) return false;
|
|
32786
|
+
if (bbox.w >= frameWidth * .95) return false;
|
|
32787
|
+
return true;
|
|
32788
|
+
}
|
|
32789
|
+
/**
|
|
32790
|
+
* Minimum fraction of the track's BEST-SEEN bbox area a retry frame's bbox must
|
|
32791
|
+
* still cover to be an acceptable best-`thumbnail` RETRY subject. 0.45 tolerates
|
|
32792
|
+
* normal breathing of the tracker box (~2/3 per side) while rejecting the
|
|
32793
|
+
* degenerate shrink (observed live 2026-07-22: a car born at 78px retried at
|
|
32794
|
+
* 26px once the native lease warmed — a ~0.11 area fraction — and the landed
|
|
32795
|
+
* "best" thumbnail was a sliver of a distant car).
|
|
32796
|
+
*/
|
|
32797
|
+
var RETRY_MIN_FRACTION_OF_BEST = .45;
|
|
32798
|
+
/**
|
|
32799
|
+
* RETRY-ONLY gate (#27-A degenerate-retry fix): on a cold on-motion session the
|
|
32800
|
+
* native lease misses the first seconds, so the large-subject captures return
|
|
32801
|
+
* null and the per-frame retry re-fires with the CURRENT frame's bbox. By the
|
|
32802
|
+
* time the lease warms the subject may have shrunk to a sliver; capturing THAT
|
|
32803
|
+
* frame lands a genuine-native-but-useless thumbnail and `thumbnailLanded`
|
|
32804
|
+
* stops all further retries. Reject a retry whose current bbox area has
|
|
32805
|
+
* collapsed below `minFractionOfBest` of the track's best-seen bbox area.
|
|
32806
|
+
*
|
|
32807
|
+
* Applies ONLY to the `!thumbnailLanded` retry path — a genuine new-best
|
|
32808
|
+
* capture is never routed through this gate (the caller bypasses it on
|
|
32809
|
+
* `isNewBest`). With no best-seen reference (or a degenerate one) there is no
|
|
32810
|
+
* info to reject on → plausible.
|
|
32811
|
+
*/
|
|
32812
|
+
function isPlausibleRetryBox(current, bestSeen, minFractionOfBest = RETRY_MIN_FRACTION_OF_BEST) {
|
|
32813
|
+
if (bestSeen === null) return true;
|
|
32814
|
+
const bestArea = bestSeen.w * bestSeen.h;
|
|
32815
|
+
if (bestArea <= 0) return true;
|
|
32816
|
+
return current.w * current.h >= minFractionOfBest * bestArea;
|
|
32817
|
+
}
|
|
32818
|
+
//#endregion
|
|
32819
|
+
//#region src/pipeline-analytics/pipeline/semantic-promotion.ts
|
|
32820
|
+
/**
|
|
32821
|
+
* Compose the {@link SemanticRecognitionFrame} for a plate read / accepted
|
|
32822
|
+
* fine classification. ONE rule, enforced here rather than at the call site:
|
|
32823
|
+
* the promoted `bbox` is ALWAYS the SUBJECT box (`frame.bbox`), never the
|
|
32824
|
+
* detail box — see {@link DetailAttributionResult.bbox} for what feeding the
|
|
32825
|
+
* detail box cost. The face path (`face-recognizer.ts` `onSemanticMatch`)
|
|
32826
|
+
* already passes its body box; this keeps the plate/classification path on the
|
|
32827
|
+
* same discipline.
|
|
32828
|
+
*/
|
|
32829
|
+
function semanticFrameForDetailAttribution(deviceId, trackId, detail, frame) {
|
|
32830
|
+
return {
|
|
32831
|
+
deviceId,
|
|
32832
|
+
trackId,
|
|
32833
|
+
timestamp: frame.timestamp,
|
|
32834
|
+
bbox: {
|
|
32835
|
+
x: frame.bbox.x,
|
|
32836
|
+
y: frame.bbox.y,
|
|
32837
|
+
w: frame.bbox.w,
|
|
32838
|
+
h: frame.bbox.h
|
|
32839
|
+
},
|
|
32840
|
+
frameWidth: frame.frameWidth,
|
|
32841
|
+
frameHeight: frame.frameHeight,
|
|
32842
|
+
kind: detail.className === "plate" ? "plate" : "classification",
|
|
32843
|
+
score: detail.score,
|
|
32844
|
+
confidence: frame.confidence,
|
|
32845
|
+
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
32846
|
+
};
|
|
32847
|
+
}
|
|
32848
|
+
/**
|
|
32849
|
+
* Offer the recognition frame to the best-frame ranking with the semantic
|
|
32850
|
+
* tier. On a win: record the best-seen bbox (plausible boxes only — an
|
|
32851
|
+
* envelope-exploded box must not poison the retry reference, mirroring
|
|
32852
|
+
* `buildSnapshotTargets`) and re-fire the captures when the pixels are still
|
|
32853
|
+
* reachable.
|
|
32854
|
+
*/
|
|
32855
|
+
function promoteSemanticBestFrame(deps, input) {
|
|
32856
|
+
const { deviceId, trackId, bbox, frameWidth, frameHeight, timestamp } = input;
|
|
32857
|
+
if (!deps.observeBestFrame({
|
|
32858
|
+
deviceId,
|
|
32859
|
+
trackId,
|
|
32860
|
+
confidence: input.confidence,
|
|
32861
|
+
timestamp,
|
|
32862
|
+
edgeClear: isEdgeClear({
|
|
32863
|
+
bbox,
|
|
32864
|
+
frameWidth,
|
|
32865
|
+
frameHeight
|
|
32866
|
+
}),
|
|
32867
|
+
centerScore: bboxCenterScore(bbox, frameWidth, frameHeight),
|
|
32868
|
+
containment: bboxContainment(bbox, frameWidth, frameHeight),
|
|
32869
|
+
sizeScore: bboxSizeScore(bbox, frameWidth, frameHeight),
|
|
32870
|
+
squareFraction: centralSquareContainmentFraction(bbox, {
|
|
32871
|
+
W: frameWidth,
|
|
32872
|
+
H: frameHeight
|
|
32873
|
+
}),
|
|
32874
|
+
semantic: true
|
|
32875
|
+
})) {
|
|
32876
|
+
deps.logger.debug("best-shot: semantic frame did not beat the held peak", {
|
|
32877
|
+
tags: { deviceId },
|
|
32878
|
+
meta: {
|
|
32879
|
+
trackId,
|
|
32880
|
+
kind: input.kind,
|
|
32881
|
+
score: input.score,
|
|
32882
|
+
timestamp
|
|
32883
|
+
}
|
|
32884
|
+
});
|
|
32885
|
+
return {
|
|
32886
|
+
promoted: false,
|
|
32887
|
+
captured: false
|
|
32888
|
+
};
|
|
32889
|
+
}
|
|
32890
|
+
const plausible = isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, input.confidence);
|
|
32891
|
+
if (plausible) deps.recordBestSeenBbox(deviceId, trackId, {
|
|
32892
|
+
x: bbox.x,
|
|
32893
|
+
y: bbox.y,
|
|
32894
|
+
w: bbox.w,
|
|
32895
|
+
h: bbox.h,
|
|
32896
|
+
frameWidth,
|
|
32897
|
+
frameHeight,
|
|
32898
|
+
timestamp
|
|
32899
|
+
});
|
|
32900
|
+
if (!plausible || input.frameHandle === void 0) {
|
|
32901
|
+
deps.logger.info("best-shot: semantic promotion without capture", {
|
|
32902
|
+
tags: { deviceId },
|
|
32903
|
+
meta: {
|
|
32904
|
+
trackId,
|
|
32905
|
+
kind: input.kind,
|
|
32906
|
+
score: input.score,
|
|
32907
|
+
timestamp,
|
|
32908
|
+
reason: plausible ? "no frame handle" : "implausible box"
|
|
32909
|
+
}
|
|
32910
|
+
});
|
|
32911
|
+
return {
|
|
32912
|
+
promoted: true,
|
|
32913
|
+
captured: false
|
|
32914
|
+
};
|
|
32915
|
+
}
|
|
32916
|
+
deps.captureBestShot({
|
|
32917
|
+
deviceId,
|
|
32918
|
+
trackId,
|
|
32919
|
+
timestamp,
|
|
32920
|
+
bbox,
|
|
32921
|
+
frameWidth,
|
|
32922
|
+
frameHeight,
|
|
32923
|
+
frameHandle: input.frameHandle
|
|
32924
|
+
});
|
|
32925
|
+
deps.logger.info("best-shot: semantic promotion", {
|
|
32926
|
+
tags: { deviceId },
|
|
32927
|
+
meta: {
|
|
32928
|
+
trackId,
|
|
32929
|
+
kind: input.kind,
|
|
32930
|
+
score: input.score,
|
|
32931
|
+
timestamp
|
|
32932
|
+
}
|
|
32933
|
+
});
|
|
32934
|
+
return {
|
|
32935
|
+
promoted: true,
|
|
32936
|
+
captured: true
|
|
32937
|
+
};
|
|
32938
|
+
}
|
|
32738
32939
|
//#endregion
|
|
32739
32940
|
//#region src/pipeline-analytics/store/zone-geometry.ts
|
|
32740
32941
|
/**
|
|
@@ -34473,6 +34674,7 @@ var PlateGalleryProvider = class {
|
|
|
34473
34674
|
refreshGallery;
|
|
34474
34675
|
emitPlateGalleryChanged;
|
|
34475
34676
|
eventMediaBaseUrl;
|
|
34677
|
+
eventMediaPathPrefix;
|
|
34476
34678
|
constructor(deps) {
|
|
34477
34679
|
this.plateStore = deps.plateStore;
|
|
34478
34680
|
this.vehicleStore = deps.vehicleStore;
|
|
@@ -34483,6 +34685,7 @@ var PlateGalleryProvider = class {
|
|
|
34483
34685
|
this.refreshGallery = deps.refreshGallery;
|
|
34484
34686
|
this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
|
|
34485
34687
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
34688
|
+
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
34486
34689
|
}
|
|
34487
34690
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
34488
34691
|
safeEmitPlateGalleryChanged(payload) {
|
|
@@ -34553,13 +34756,7 @@ var PlateGalleryProvider = class {
|
|
|
34553
34756
|
return this.toPlateInfo(plate, nameMap, { includeCrops: false });
|
|
34554
34757
|
}
|
|
34555
34758
|
async getPlateMedia(input) {
|
|
34556
|
-
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => (
|
|
34557
|
-
key: m.key,
|
|
34558
|
-
kind: m.kind,
|
|
34559
|
-
base64: m.base64,
|
|
34560
|
-
sizeBytes: m.sizeBytes,
|
|
34561
|
-
timestamp: m.timestamp
|
|
34562
|
-
}));
|
|
34759
|
+
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
|
|
34563
34760
|
}
|
|
34564
34761
|
async searchPlates(input) {
|
|
34565
34762
|
const maxDistance = input.maxDistance ?? 1;
|
|
@@ -34802,6 +34999,51 @@ var PlateGalleryProvider = class {
|
|
|
34802
34999
|
}
|
|
34803
35000
|
};
|
|
34804
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
|
|
34805
35047
|
//#region src/pipeline-analytics/retention/disk-reconcile.ts
|
|
34806
35048
|
/**
|
|
34807
35049
|
* Disk-wins reconcile for pipeline analytics.
|
|
@@ -34866,92 +35108,6 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
34866
35108
|
};
|
|
34867
35109
|
}
|
|
34868
35110
|
//#endregion
|
|
34869
|
-
//#region src/pipeline-analytics/retention/media-reclaim-actions.ts
|
|
34870
|
-
/**
|
|
34871
|
-
* The operator button for the debug-media reclaim.
|
|
34872
|
-
*
|
|
34873
|
-
* The mechanism ships armed with nothing: **arming it is the operator's
|
|
34874
|
-
* call**. `mode` omitted means `report` — it counts, logs one line per camera
|
|
34875
|
-
* and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
|
|
34876
|
-
* that has to be typed.
|
|
34877
|
-
*
|
|
34878
|
-
* Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
|
|
34879
|
-
* earned it: dry-run default, explicit pacing, explicit blast radius, and a
|
|
34880
|
-
* report you can read instead of a log line you have to find.
|
|
34881
|
-
*/
|
|
34882
|
-
/**
|
|
34883
|
-
* `report` counts; `reclaim` deletes.
|
|
34884
|
-
*
|
|
34885
|
-
* `.default('report')` is load-bearing: the settings form sends `{}` for a
|
|
34886
|
-
* button with no fields, and the safe reading of an unstated intent is the one
|
|
34887
|
-
* that destroys nothing.
|
|
34888
|
-
*/
|
|
34889
|
-
var MediaReclaimActionModeSchema = _enum(["report", "reclaim"]).default("report");
|
|
34890
|
-
var MediaReclaimScopeSchema = _enum(["motion-still", "track-filmstrip"]);
|
|
34891
|
-
var MediaReclaimActionInputSchema = object({
|
|
34892
|
-
mode: MediaReclaimActionModeSchema,
|
|
34893
|
-
/** Which populations to walk. Omitted = both. */
|
|
34894
|
-
scopes: array(MediaReclaimScopeSchema).min(1).optional(),
|
|
34895
|
-
/** Restrict to these cameras. Omitted = every camera in retention scope. */
|
|
34896
|
-
deviceIds: array(number().int()).min(1).optional(),
|
|
34897
|
-
/** Start every device from the top instead of resuming its cursor. */
|
|
34898
|
-
restart: boolean().optional(),
|
|
34899
|
-
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
34900
|
-
pageSize: number().int().min(50).max(5e3).optional(),
|
|
34901
|
-
/** Rows EXAMINED per camera in this pass — the pacing bound. */
|
|
34902
|
-
maxRowsPerDevice: number().int().min(1).max(5e5).optional(),
|
|
34903
|
-
/** Rows DELETED per camera in this pass — the blast-radius bound. */
|
|
34904
|
-
maxReclaimPerDevice: number().int().min(1).max(5e5).optional(),
|
|
34905
|
-
/** Bytes released across the whole run — the disk-contention bound. */
|
|
34906
|
-
maxBytesPerRun: number().int().min(1).optional(),
|
|
34907
|
-
/** Wall-clock ceiling for the run. */
|
|
34908
|
-
budgetMinutes: number().int().min(1).max(720).optional(),
|
|
34909
|
-
/**
|
|
34910
|
-
* Bytes per second the pass may release. The recordings volume sustains
|
|
34911
|
-
* ~4 MB/s and the archive needs it more than this does; the default is half
|
|
34912
|
-
* of that.
|
|
34913
|
-
*/
|
|
34914
|
-
throttleBytesPerSec: number().int().min(64 * 1024).optional(),
|
|
34915
|
-
/**
|
|
34916
|
-
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
34917
|
-
* exclusively save rows, never select them.
|
|
34918
|
-
*/
|
|
34919
|
-
graceMinutes: number().int().min(1).max(10080).optional()
|
|
34920
|
-
});
|
|
34921
|
-
var MediaReclaimDeviceReportSchema = object({
|
|
34922
|
-
deviceId: number(),
|
|
34923
|
-
scope: MediaReclaimScopeSchema,
|
|
34924
|
-
examined: number(),
|
|
34925
|
-
eligible: number(),
|
|
34926
|
-
reclaimed: number(),
|
|
34927
|
-
bytesReclaimed: number(),
|
|
34928
|
-
/** Rows kept because the owning track is pinned (`staging`/`favourited`). */
|
|
34929
|
-
held: number(),
|
|
34930
|
-
/** Rows kept because the owner could not be shown to be a motion event. */
|
|
34931
|
-
unresolved: number(),
|
|
34932
|
-
withinGrace: number(),
|
|
34933
|
-
/** Deletes that REFUSED — the blob survived or the volume could not answer.
|
|
34934
|
-
* The row was kept and the cursor did not pass it. */
|
|
34935
|
-
refused: number(),
|
|
34936
|
-
stoppedBy: string(),
|
|
34937
|
-
/** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
|
|
34938
|
-
nextCursor: number()
|
|
34939
|
-
});
|
|
34940
|
-
var mediaReclaimActions = defineCustomActions({ "retention.mediaReclaim": customAction(MediaReclaimActionInputSchema, object({
|
|
34941
|
-
/** `false` when a pass was already in flight — nothing was done. */
|
|
34942
|
-
ran: boolean(),
|
|
34943
|
-
mode: _enum(["report", "reclaim"]),
|
|
34944
|
-
devices: array(MediaReclaimDeviceReportSchema),
|
|
34945
|
-
totalExamined: number(),
|
|
34946
|
-
totalEligible: number(),
|
|
34947
|
-
totalReclaimed: number(),
|
|
34948
|
-
totalBytesReclaimed: number(),
|
|
34949
|
-
totalRefused: number(),
|
|
34950
|
-
durationMs: number(),
|
|
34951
|
-
/** Every device+scope reported `exhausted` — there is nothing left to do. */
|
|
34952
|
-
complete: boolean()
|
|
34953
|
-
}), { auth: "admin" }) });
|
|
34954
|
-
//#endregion
|
|
34955
35111
|
//#region src/pipeline-analytics/retention/media-reclaim.ts
|
|
34956
35112
|
var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
|
|
34957
35113
|
var DEFAULT_MEDIA_RECLAIM_LIMITS = {
|
|
@@ -35162,6 +35318,92 @@ async function runMediaReclaim(input) {
|
|
|
35162
35318
|
cursors: nextCursors
|
|
35163
35319
|
};
|
|
35164
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" }) });
|
|
35165
35407
|
var OrphanAuditActionInputSchema = object({
|
|
35166
35408
|
mode: _enum(["report", "reclaim"]).default("report"),
|
|
35167
35409
|
/**
|
|
@@ -36481,6 +36723,28 @@ function trackMediaScopesForKinds(trackId, kinds) {
|
|
|
36481
36723
|
return kinds.some((kind) => DETAIL_CROP_KINDS.includes(kind)) ? scopes : scopes.filter((scope) => scope.publishCropAs === null);
|
|
36482
36724
|
}
|
|
36483
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
|
+
/**
|
|
36484
36748
|
* Fold every scope's rows into ONE track media listing: detail crops
|
|
36485
36749
|
* relabelled, everything filtered to `kinds`, ordered oldest first.
|
|
36486
36750
|
*
|
|
@@ -36617,8 +36881,10 @@ var AnalyticsQueryFacade = class {
|
|
|
36617
36881
|
return this.withMediaUrl(events);
|
|
36618
36882
|
}
|
|
36619
36883
|
async getEventMedia(input) {
|
|
36620
|
-
const
|
|
36621
|
-
|
|
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));
|
|
36622
36888
|
}
|
|
36623
36889
|
/**
|
|
36624
36890
|
* A track's media WITH the bytes, restricted to `kinds` when given.
|
|
@@ -36631,14 +36897,36 @@ var AnalyticsQueryFacade = class {
|
|
|
36631
36897
|
async getTrackMedia(input) {
|
|
36632
36898
|
const store = this.deps.mediaStore();
|
|
36633
36899
|
if (store === null) return [];
|
|
36900
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36634
36901
|
const scopes = trackMediaScopesForKinds(input.trackId, input.kinds);
|
|
36635
|
-
return collectTrackMedia(await Promise.all(scopes.map(async (scope) =>
|
|
36636
|
-
scope,
|
|
36637
|
-
|
|
36638
|
-
|
|
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) => ({
|
|
36639
36913
|
...row,
|
|
36640
36914
|
kind
|
|
36641
|
-
}), 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));
|
|
36642
36930
|
}
|
|
36643
36931
|
/**
|
|
36644
36932
|
* A track's media WITHOUT the bytes.
|
|
@@ -36656,13 +36944,14 @@ var AnalyticsQueryFacade = class {
|
|
|
36656
36944
|
async listTrackMedia(input) {
|
|
36657
36945
|
const store = this.deps.mediaStore();
|
|
36658
36946
|
if (store === null) return [];
|
|
36947
|
+
const prefix = this.deps.eventMediaPathPrefix();
|
|
36659
36948
|
return collectTrackMedia(await Promise.all(trackMediaOwnerScopes(input.trackId).map(async (scope) => ({
|
|
36660
36949
|
scope,
|
|
36661
36950
|
rows: await store.listInfoByOwner(scope.ownerKind, scope.ownerId, input.deviceId)
|
|
36662
36951
|
}))), (row, kind) => ({
|
|
36663
36952
|
...row,
|
|
36664
36953
|
kind
|
|
36665
|
-
}), void 0);
|
|
36954
|
+
}), void 0).map((row) => toWireMediaFileInfo(row, prefix));
|
|
36666
36955
|
}
|
|
36667
36956
|
async pruneEventsBefore(input) {
|
|
36668
36957
|
const eventStore = this.deps.eventStore();
|
|
@@ -39107,6 +39396,31 @@ function createEventMediaHandler(deps) {
|
|
|
39107
39396
|
}
|
|
39108
39397
|
//#endregion
|
|
39109
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
|
+
}
|
|
39110
39424
|
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
39111
39425
|
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
39112
39426
|
* for new tracks (served only for old stored rows). Undefined when the event
|
|
@@ -39157,15 +39471,24 @@ async function collectCropForcedMedia(source, id) {
|
|
|
39157
39471
|
};
|
|
39158
39472
|
}
|
|
39159
39473
|
/**
|
|
39160
|
-
*
|
|
39161
|
-
* has any, else the owning track's
|
|
39162
|
-
*
|
|
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.
|
|
39163
39481
|
*/
|
|
39164
|
-
async function
|
|
39165
|
-
|
|
39166
|
-
|
|
39482
|
+
async function collectDefaultEventMedia(deps) {
|
|
39483
|
+
if (deps.eventFiles.length > 0) return {
|
|
39484
|
+
candidates: deps.eventFiles,
|
|
39485
|
+
pick: pickEventOwnedMedia
|
|
39486
|
+
};
|
|
39167
39487
|
const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
|
|
39168
|
-
return
|
|
39488
|
+
return {
|
|
39489
|
+
candidates: await deps.listTrackMedia(trackId),
|
|
39490
|
+
pick: pickTrackFallbackMedia
|
|
39491
|
+
};
|
|
39169
39492
|
}
|
|
39170
39493
|
//#endregion
|
|
39171
39494
|
//#region src/pipeline-analytics/face-settings.ts
|
|
@@ -41066,13 +41389,29 @@ var RETRAIN_FRAME_COLUMNS = [
|
|
|
41066
41389
|
type: "TEXT"
|
|
41067
41390
|
})
|
|
41068
41391
|
];
|
|
41069
|
-
var RETRAIN_FRAME_INDEXES = [
|
|
41070
|
-
|
|
41071
|
-
|
|
41072
|
-
|
|
41073
|
-
|
|
41074
|
-
|
|
41075
|
-
|
|
41392
|
+
var RETRAIN_FRAME_INDEXES = [
|
|
41393
|
+
{
|
|
41394
|
+
name: "idx_retrain_frames_track",
|
|
41395
|
+
columns: ["trackId"]
|
|
41396
|
+
},
|
|
41397
|
+
{
|
|
41398
|
+
name: "idx_retrain_frames_device",
|
|
41399
|
+
columns: ["deviceId", "copiedAt"]
|
|
41400
|
+
},
|
|
41401
|
+
(
|
|
41402
|
+
/**
|
|
41403
|
+
* The seal gate's other half. This collection is orders of magnitude smaller
|
|
41404
|
+
* than `pipeline-analytics:media` — every row is an operator-staged frame —
|
|
41405
|
+
* so the index buys much less here. It exists anyway because the gate reads
|
|
41406
|
+
* the two lanes with ONE query shape, and a lane that answers by scanning is
|
|
41407
|
+
* a lane that starts timing out the day the dataset grows. See
|
|
41408
|
+
* `MEDIA_INDEXES` for the cost this is a small multiple of.
|
|
41409
|
+
*/
|
|
41410
|
+
{
|
|
41411
|
+
name: "idx_retrain_frames_location",
|
|
41412
|
+
columns: ["locationId"]
|
|
41413
|
+
})
|
|
41414
|
+
];
|
|
41076
41415
|
/**
|
|
41077
41416
|
* The copy's id, derived from `(trackId, sourceMediaKey)`.
|
|
41078
41417
|
*
|
|
@@ -44337,14 +44676,27 @@ function snapshot(j) {
|
|
|
44337
44676
|
};
|
|
44338
44677
|
}
|
|
44339
44678
|
/**
|
|
44679
|
+
* The column both walked collections use for the storage location.
|
|
44680
|
+
*
|
|
44681
|
+
* Named once because it is now a QUERY PREDICATE (`where: { [it]: null }`) as
|
|
44682
|
+
* well as a row field. A predicate naming a column the collection cannot
|
|
44683
|
+
* express is refused by `count` rather than dropped, so a rename that misses
|
|
44684
|
+
* one site fails loudly instead of counting the whole table.
|
|
44685
|
+
*/
|
|
44686
|
+
var LOCATION_ID_FIELD = "locationId";
|
|
44687
|
+
/**
|
|
44340
44688
|
* True when the row names NO location — the population `seal` exists for.
|
|
44341
44689
|
*
|
|
44342
44690
|
* Deliberately NOT `mediaRowLocation(data) === DEFAULT_MEDIA_LOCATION`: that
|
|
44343
44691
|
* helper collapses "unstamped" and "stamped with the bare type ref" into one
|
|
44344
44692
|
* answer, and only the first is a row whose meaning changes under a repoint.
|
|
44693
|
+
*
|
|
44694
|
+
* The in-JS twin of the `IS NULL` predicate {@link MediaRelocateEngine.countUnstamped}
|
|
44695
|
+
* sends. It stays because the PASS still walks rows and must decide per row;
|
|
44696
|
+
* the two must agree, and `unstamped-count-matches-the-pass` asserts they do.
|
|
44345
44697
|
*/
|
|
44346
44698
|
function rowIsUnstamped(data) {
|
|
44347
|
-
const id = data[
|
|
44699
|
+
const id = data[LOCATION_ID_FIELD];
|
|
44348
44700
|
return !(typeof id === "string" && id.length > 0);
|
|
44349
44701
|
}
|
|
44350
44702
|
/** True for the enrolled-gallery rows `move` skips and `gallery` selects. */
|
|
@@ -44402,26 +44754,91 @@ var MediaRelocateEngine = class {
|
|
|
44402
44754
|
return true;
|
|
44403
44755
|
}
|
|
44404
44756
|
/**
|
|
44405
|
-
*
|
|
44757
|
+
* Rows that still carry no `locationId`, per collection.
|
|
44406
44758
|
*
|
|
44407
|
-
* Read-only, and the ONLY
|
|
44408
|
-
* is allowed to trust: it is
|
|
44759
|
+
* Read-only, and the ONLY answer the migration planner's non-blocking gate
|
|
44760
|
+
* is allowed to trust: it is measured here, against the rows themselves,
|
|
44409
44761
|
* rather than inferred from "a seal job reported done" — a seal that failed
|
|
44410
44762
|
* some rows still reports a terminal state.
|
|
44763
|
+
*
|
|
44764
|
+
* ── Why this is not the walk the rest of this file uses ──────────────────
|
|
44765
|
+
*
|
|
44766
|
+
* It used to be. It paged both collections through {@link pageRows} — 200
|
|
44767
|
+
* rows per RPC, ordered by `timestamp`, a column that leads no index — and
|
|
44768
|
+
* counted `rowIsUnstamped` in JS. On the live hub that is ~6 300 round trips
|
|
44769
|
+
* over 1 254 576 rows, each paying its own sort, and it hit the 60 s RPC
|
|
44770
|
+
* deadline **every time it was called since it shipped**. The gate it exists
|
|
44771
|
+
* to serve could therefore never open, and 152 GB of event media could not
|
|
44772
|
+
* be migrated.
|
|
44773
|
+
*
|
|
44774
|
+
* The walk is right for the PASS, which must visit every row to act on it.
|
|
44775
|
+
* It was never right for a question the store can answer with a predicate,
|
|
44776
|
+
* and the predicate is now expressible: `where: { locationId: null }`
|
|
44777
|
+
* compiles to `IS NULL` (it used to compile to `= NULL`, which matches
|
|
44778
|
+
* nothing and reports it as an answer — the number the gate treats as "safe
|
|
44779
|
+
* to cut over"). With `idx_media_location` in place both statements read a
|
|
44780
|
+
* contiguous range from the head of a b-tree.
|
|
44781
|
+
*
|
|
44782
|
+
* The cheap question is asked FIRST and on its own. `present` is a single
|
|
44783
|
+
* seek and decides the gate; `rows` is a second statement and only decorates
|
|
44784
|
+
* the refusal. So a count that cannot be taken still leaves a usable answer
|
|
44785
|
+
* — `{ present: true, rows: null }` refuses just as hard — and `null` never
|
|
44786
|
+
* degrades into `0`.
|
|
44411
44787
|
*/
|
|
44412
44788
|
async countUnstamped() {
|
|
44413
|
-
|
|
44414
|
-
|
|
44415
|
-
|
|
44416
|
-
|
|
44417
|
-
|
|
44418
|
-
|
|
44419
|
-
|
|
44789
|
+
try {
|
|
44790
|
+
const media = await this.countUnstampedIn(MEDIA_COLLECTION);
|
|
44791
|
+
const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION);
|
|
44792
|
+
return {
|
|
44793
|
+
media,
|
|
44794
|
+
retrainFrames,
|
|
44795
|
+
anyPresent: media.present || retrainFrames.present,
|
|
44796
|
+
total: media.rows === null || retrainFrames.rows === null ? null : media.rows + retrainFrames.rows
|
|
44797
|
+
};
|
|
44798
|
+
} catch (err) {
|
|
44799
|
+
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) } });
|
|
44800
|
+
return null;
|
|
44801
|
+
}
|
|
44420
44802
|
}
|
|
44421
|
-
|
|
44422
|
-
|
|
44423
|
-
|
|
44424
|
-
|
|
44803
|
+
/**
|
|
44804
|
+
* One collection's unstamped population: existence, then size.
|
|
44805
|
+
*
|
|
44806
|
+
* The `limit: 1` probe is deliberately a `query` and not a `count`: a count
|
|
44807
|
+
* of a million matching rows still walks a million index entries, while the
|
|
44808
|
+
* probe stops at the first one. Absence is proved by the same seek finding
|
|
44809
|
+
* nothing, which is where the index earns its keep — after a seal, the range
|
|
44810
|
+
* is empty and the answer is one b-tree descent.
|
|
44811
|
+
*/
|
|
44812
|
+
async countUnstampedIn(collection) {
|
|
44813
|
+
if ((await this.deps.store.query.query({
|
|
44814
|
+
collection,
|
|
44815
|
+
filter: {
|
|
44816
|
+
where: { [LOCATION_ID_FIELD]: null },
|
|
44817
|
+
limit: 1
|
|
44818
|
+
},
|
|
44819
|
+
columns: [LOCATION_ID_FIELD]
|
|
44820
|
+
})).length === 0) return {
|
|
44821
|
+
present: false,
|
|
44822
|
+
rows: 0
|
|
44823
|
+
};
|
|
44824
|
+
try {
|
|
44825
|
+
return {
|
|
44826
|
+
present: true,
|
|
44827
|
+
rows: await this.deps.store.count.query({
|
|
44828
|
+
collection,
|
|
44829
|
+
filter: { where: { [LOCATION_ID_FIELD]: null } }
|
|
44830
|
+
})
|
|
44831
|
+
};
|
|
44832
|
+
} catch (err) {
|
|
44833
|
+
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: {
|
|
44834
|
+
collection,
|
|
44835
|
+
error: String(err)
|
|
44836
|
+
} });
|
|
44837
|
+
return {
|
|
44838
|
+
present: true,
|
|
44839
|
+
rows: null
|
|
44840
|
+
};
|
|
44841
|
+
}
|
|
44425
44842
|
}
|
|
44426
44843
|
start(input) {
|
|
44427
44844
|
for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
|
|
@@ -47178,6 +47595,70 @@ var PackageDropDetector = class {
|
|
|
47178
47595
|
}
|
|
47179
47596
|
};
|
|
47180
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
|
|
47181
47662
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
|
|
47182
47663
|
/**
|
|
47183
47664
|
* stationary-evidence — how a parked object EARNS a registry entry.
|
|
@@ -49709,83 +50190,6 @@ function buildRecordedStillPorts(input) {
|
|
|
49709
50190
|
})
|
|
49710
50191
|
};
|
|
49711
50192
|
}
|
|
49712
|
-
function errorMessage(err) {
|
|
49713
|
-
return err instanceof Error ? err.message : String(err);
|
|
49714
|
-
}
|
|
49715
|
-
async function reconcileGallerySamples(deps, options) {
|
|
49716
|
-
let checked = 0;
|
|
49717
|
-
let reclaimed = 0;
|
|
49718
|
-
let keyless = 0;
|
|
49719
|
-
let skippedGrace = 0;
|
|
49720
|
-
let stoppedByCeiling = false;
|
|
49721
|
-
const identities = await deps.listIdentities();
|
|
49722
|
-
for (const identity of identities) {
|
|
49723
|
-
const samples = await deps.listSamples(identity.id);
|
|
49724
|
-
for (const sample of samples) {
|
|
49725
|
-
if (sample.mediaKey === void 0) {
|
|
49726
|
-
keyless += 1;
|
|
49727
|
-
continue;
|
|
49728
|
-
}
|
|
49729
|
-
if (options.now - sample.addedAt < options.graceMs) {
|
|
49730
|
-
skippedGrace += 1;
|
|
49731
|
-
continue;
|
|
49732
|
-
}
|
|
49733
|
-
if (reclaimed >= options.maxReclaimPerRun) {
|
|
49734
|
-
stoppedByCeiling = true;
|
|
49735
|
-
deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
|
|
49736
|
-
return {
|
|
49737
|
-
checked,
|
|
49738
|
-
reclaimed,
|
|
49739
|
-
keyless,
|
|
49740
|
-
skippedGrace,
|
|
49741
|
-
stoppedByCeiling
|
|
49742
|
-
};
|
|
49743
|
-
}
|
|
49744
|
-
let state;
|
|
49745
|
-
try {
|
|
49746
|
-
state = await deps.blobState(sample.mediaKey);
|
|
49747
|
-
} catch (err) {
|
|
49748
|
-
const reason = errorMessage(err);
|
|
49749
|
-
deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
|
|
49750
|
-
sampleId: sample.id,
|
|
49751
|
-
identity: identity.name,
|
|
49752
|
-
error: reason
|
|
49753
|
-
} });
|
|
49754
|
-
return {
|
|
49755
|
-
checked,
|
|
49756
|
-
reclaimed,
|
|
49757
|
-
keyless,
|
|
49758
|
-
skippedGrace,
|
|
49759
|
-
stoppedByCeiling,
|
|
49760
|
-
aborted: reason
|
|
49761
|
-
};
|
|
49762
|
-
}
|
|
49763
|
-
checked += 1;
|
|
49764
|
-
if (state === "exists") continue;
|
|
49765
|
-
await deps.removeSample(identity.id, sample.id);
|
|
49766
|
-
reclaimed += 1;
|
|
49767
|
-
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
49768
|
-
sampleId: sample.id,
|
|
49769
|
-
identity: identity.name,
|
|
49770
|
-
mediaKey: sample.mediaKey,
|
|
49771
|
-
state
|
|
49772
|
-
} });
|
|
49773
|
-
}
|
|
49774
|
-
}
|
|
49775
|
-
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
49776
|
-
checked,
|
|
49777
|
-
reclaimed,
|
|
49778
|
-
keyless,
|
|
49779
|
-
skippedGrace
|
|
49780
|
-
} });
|
|
49781
|
-
return {
|
|
49782
|
-
checked,
|
|
49783
|
-
reclaimed,
|
|
49784
|
-
keyless,
|
|
49785
|
-
skippedGrace,
|
|
49786
|
-
stoppedByCeiling
|
|
49787
|
-
};
|
|
49788
|
-
}
|
|
49789
50193
|
//#endregion
|
|
49790
50194
|
//#region src/pipeline-analytics/retention/embedding-orphan-scan.ts
|
|
49791
50195
|
/** The scope id this leg reports and persists its cursor under. */
|
|
@@ -49902,6 +50306,83 @@ async function scanEmbeddingOrphans(deps, limits, cursor) {
|
|
|
49902
50306
|
nextCursor: Math.max(0, offset)
|
|
49903
50307
|
};
|
|
49904
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
|
+
}
|
|
49905
50386
|
/**
|
|
49906
50387
|
* Where the completion marker lives.
|
|
49907
50388
|
*
|
|
@@ -52964,14 +53445,14 @@ function isAfterCursor(row, cursor) {
|
|
|
52964
53445
|
if (row.lastSeen < cursor.lastSeen) return true;
|
|
52965
53446
|
return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
|
|
52966
53447
|
}
|
|
52967
|
-
var VEHICLE_CLASSES
|
|
53448
|
+
var VEHICLE_CLASSES = new Set([
|
|
52968
53449
|
"vehicle",
|
|
52969
53450
|
"car",
|
|
52970
53451
|
"truck",
|
|
52971
53452
|
"bus"
|
|
52972
53453
|
]);
|
|
52973
53454
|
function motionlessMaxPxForClass(className) {
|
|
52974
|
-
return VEHICLE_CLASSES
|
|
53455
|
+
return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
|
|
52975
53456
|
}
|
|
52976
53457
|
function netDisplacementPx(positions) {
|
|
52977
53458
|
const first = positions[0];
|
|
@@ -56731,70 +57212,6 @@ function hasWrappedDetection(entries) {
|
|
|
56731
57212
|
return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
|
|
56732
57213
|
}
|
|
56733
57214
|
//#endregion
|
|
56734
|
-
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
56735
|
-
/**
|
|
56736
|
-
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
56737
|
-
*
|
|
56738
|
-
* 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
|
|
56739
|
-
* routinely pokes a little outside the vehicle box, and requiring total
|
|
56740
|
-
* containment would count almost none of them. 0.8 and not 0.5: half a person
|
|
56741
|
-
* box overlapping a car is what a pedestrian walking in front of one looks
|
|
56742
|
-
* like, and counting those would make the number mean "somebody was near a
|
|
56743
|
-
* car", which is not a defect.
|
|
56744
|
-
*/
|
|
56745
|
-
var CONTAINMENT_THRESHOLD = .8;
|
|
56746
|
-
/**
|
|
56747
|
-
* The classes that count as a vehicle for this test.
|
|
56748
|
-
*
|
|
56749
|
-
* A local list rather than a shared taxonomy on purpose: it exists to describe
|
|
56750
|
-
* ONE measurement, and a shared enum would make a change here silently change
|
|
56751
|
-
* detection behaviour somewhere else.
|
|
56752
|
-
*/
|
|
56753
|
-
var VEHICLE_CLASSES = [
|
|
56754
|
-
"car",
|
|
56755
|
-
"truck",
|
|
56756
|
-
"bus",
|
|
56757
|
-
"motorcycle",
|
|
56758
|
-
"vehicle"
|
|
56759
|
-
];
|
|
56760
|
-
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
56761
|
-
function intersectionArea(a, b) {
|
|
56762
|
-
const left = Math.max(a.x, b.x);
|
|
56763
|
-
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
56764
|
-
const top = Math.max(a.y, b.y);
|
|
56765
|
-
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
56766
|
-
if (right <= left || bottom <= top) return 0;
|
|
56767
|
-
return (right - left) * (bottom - top);
|
|
56768
|
-
}
|
|
56769
|
-
/**
|
|
56770
|
-
* Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
|
|
56771
|
-
*
|
|
56772
|
-
* `0` for a degenerate person box: a zero-area box is inside everything and
|
|
56773
|
-
* inside nothing, and answering `1` would count every one of them.
|
|
56774
|
-
*/
|
|
56775
|
-
function containedFraction(person, vehicle) {
|
|
56776
|
-
const area = person.w * person.h;
|
|
56777
|
-
if (area <= 0) return 0;
|
|
56778
|
-
return intersectionArea(person, vehicle) / area;
|
|
56779
|
-
}
|
|
56780
|
-
/**
|
|
56781
|
-
* Is this person box contained in any vehicle box in the same frame?
|
|
56782
|
-
*
|
|
56783
|
-
* Returns the fraction for the BEST-containing vehicle when it clears
|
|
56784
|
-
* {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
|
|
56785
|
-
* a counter that only ever says "yes" cannot later be re-thresholded against
|
|
56786
|
-
* what was actually measured.
|
|
56787
|
-
*/
|
|
56788
|
-
function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
|
|
56789
|
-
let best = 0;
|
|
56790
|
-
for (const neighbour of neighbours) {
|
|
56791
|
-
if (!VEHICLE_CLASSES.includes(neighbour.className)) continue;
|
|
56792
|
-
const fraction = containedFraction(person, neighbour.bbox);
|
|
56793
|
-
if (fraction > best) best = fraction;
|
|
56794
|
-
}
|
|
56795
|
-
return best >= threshold ? best : null;
|
|
56796
|
-
}
|
|
56797
|
-
//#endregion
|
|
56798
57215
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
56799
57216
|
function createCaptureCrop(deps) {
|
|
56800
57217
|
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
@@ -63236,6 +63653,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
63236
63653
|
* Set once the data-plane is registered; null until then (e.g. no
|
|
63237
63654
|
* dataPlane facility in the current environment). */
|
|
63238
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
|
+
}
|
|
63239
63670
|
lastActiveTrackIds = /* @__PURE__ */ new Map();
|
|
63240
63671
|
/** See `pipeline/suppressed-births.ts` — rejected births must not be
|
|
63241
63672
|
* re-upserted, and must be forgotten when the tracker drops the id. */
|
|
@@ -63486,6 +63917,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
63486
63917
|
sensorEventStore: () => this.sensorEventStore,
|
|
63487
63918
|
opsLog: () => this.eventsOpsLog,
|
|
63488
63919
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
63920
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
63489
63921
|
logger: () => this.ctx.logger,
|
|
63490
63922
|
zoneCatalogue: () => this.zoneCatalogue
|
|
63491
63923
|
});
|
|
@@ -66028,6 +66460,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66028
66460
|
logger: this.ctx.logger,
|
|
66029
66461
|
refreshGallery: () => this.faceRecognizer?.refreshGallery(),
|
|
66030
66462
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66463
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66031
66464
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload)
|
|
66032
66465
|
});
|
|
66033
66466
|
this.faceGalleryProvider = faceGallery;
|
|
@@ -66042,6 +66475,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66042
66475
|
this.plateRecognizer?.refreshGallery();
|
|
66043
66476
|
},
|
|
66044
66477
|
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
66478
|
+
eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
|
|
66045
66479
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
|
|
66046
66480
|
});
|
|
66047
66481
|
return [
|
|
@@ -69727,6 +70161,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
69727
70161
|
async listTrackMedia(input) {
|
|
69728
70162
|
return this.queryFacade.listTrackMedia(input);
|
|
69729
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
|
+
}
|
|
69730
70169
|
/**
|
|
69731
70170
|
* Search object events by text using CLIP cosine similarity.
|
|
69732
70171
|
*
|
|
@@ -71447,31 +71886,44 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71447
71886
|
return media;
|
|
71448
71887
|
}
|
|
71449
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
|
+
*/
|
|
71450
71901
|
async readEventThumbnail(id, preferKind) {
|
|
71451
|
-
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);
|
|
71452
71912
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
71453
71913
|
const layers = await collectCropForcedMedia({
|
|
71454
|
-
listByOwner:
|
|
71455
|
-
getTrackIdForEvent
|
|
71914
|
+
listByOwner: listInfo,
|
|
71915
|
+
getTrackIdForEvent
|
|
71456
71916
|
}, id);
|
|
71457
|
-
const
|
|
71458
|
-
|
|
71459
|
-
return {
|
|
71460
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71461
|
-
key: chosen.key
|
|
71462
|
-
};
|
|
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);
|
|
71463
71919
|
}
|
|
71464
|
-
const
|
|
71920
|
+
const { candidates, pick } = await collectDefaultEventMedia({
|
|
71465
71921
|
eventId: id,
|
|
71466
71922
|
eventFiles,
|
|
71467
|
-
listTrackMedia: (trackId) =>
|
|
71468
|
-
getTrackIdForEvent
|
|
71923
|
+
listTrackMedia: (trackId) => listInfo("track", trackId),
|
|
71924
|
+
getTrackIdForEvent
|
|
71469
71925
|
});
|
|
71470
|
-
|
|
71471
|
-
return {
|
|
71472
|
-
bytes: Buffer.from(chosen.base64, "base64"),
|
|
71473
|
-
key: chosen.key
|
|
71474
|
-
};
|
|
71926
|
+
return await readWinningBlob(candidates, pick, readBlob);
|
|
71475
71927
|
}
|
|
71476
71928
|
globalSettingsSchema(cap) {
|
|
71477
71929
|
return this.schema(buildGlobalSettingsSchema(cap));
|