@camstack/addon-post-analysis 1.2.91 → 1.2.92
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-CTIUuGRB.js → dist-BfrtqGyh.js} +32 -17
- package/dist/{dist-CquggoRC.mjs → dist-VnaTfwAL.mjs} +32 -17
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Q3EjHtct.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BIDGLXRs.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-D2EZ3nOf.mjs → hostInit-CpWvyhen.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +358 -160
- package/dist/pipeline-analytics/index.mjs +321 -123
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as faceGalleryCapability, At as partialRecord, B as VISIT_MERGE_GAP_MS, C as NcSnoozeSchema, Ct as _enum, D as OpsLogEntrySchema, Dt as literal, E as NcTaxonomySchema, Et as discriminatedUnion, F as TIMELAPSE_DENSE_FLOOR_SEC, G as audioModeOf, H as alarmPanelCapability, I as TimelapseRuleInputSchema, J as customAction, K as buildEventKindDescriptor, L as TimelapseRulePatchSchema, M as SCENE_DEFAULT_UNCOVERED_POLICY, Mt as string, N as SCENE_DIVERGED, Nt as unknown, O as RECORDING_EXPORT_MAX_READ_BYTES, Ot as number, P as SceneMonitorSchema, Pt as EventCategory, Q as encodeVectorBase64, R as TimelapseRuleSchema, S as NcSnoozeInputSchema, St as sleep, T as NcSystemEventKindSchema, Tt as boolean, U as assertTimelapseCadences, V as addonWidgetsSourceCapability, W as audioMetricsCapability, X as deriveRecordingMode, Y as defineCustomActions, _ as NcRuleInputSchema, _t as DeviceType, at as pipelineAnalyticsCapability, b as NcRuleTargetSchema, bt as isDeviceScopedCap, c as EVENT_PAD_MS, ct as readTimelapseGeneratedAt, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as systemEventFilterApplies, f as NC_CONDITION_CATALOG, ft as vectorDimFromBase64, g as NcConditionDescriptorSchema, gt as BaseAddon, h as NC_TAXONOMY, ht as errMsg, i as DETECTION_MACRO_CLASSES, it as notificationRulesCapability, j as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as record, k as RetrainStatusSchema, kt as object, l as LabelAttributionSchema, lt as sceneMonitorCapability, mt as zoneAnalyticsCapability, n as DEFAULT_EVENT_COLOR, nt as isScheduleActive, o as DeclaredDevices, ot as plateGalleryCapability, p as NC_DEFAULT_SNOOZE_MINUTES, pt as videoclipsCapability, q as cosineSimilarity$1, rt as kebabToCamel, s as EVENT_KIND_BY_CAP, st as readDeviceStateFrom, t as BaseDevice, tt as isDetectionMacroClass, u as MACRO_LABELS, ut as subKindsOf, v as NcRulePatchSchema, vt as createEvent, w as NcSnoozeSuppressedSchema, wt as array, x as NcScheduleSchema, xt as nodePin, y as NcRuleSchema, yt as hydrateSchema, z as TrackSourceSchema } from "../dist-
|
|
1
|
+
import { $ as faceGalleryCapability, At as partialRecord, B as VISIT_MERGE_GAP_MS, C as NcSnoozeSchema, Ct as _enum, D as OpsLogEntrySchema, Dt as literal, E as NcTaxonomySchema, Et as discriminatedUnion, F as TIMELAPSE_DENSE_FLOOR_SEC, G as audioModeOf, H as alarmPanelCapability, I as TimelapseRuleInputSchema, J as customAction, K as buildEventKindDescriptor, L as TimelapseRulePatchSchema, M as SCENE_DEFAULT_UNCOVERED_POLICY, Mt as string, N as SCENE_DIVERGED, Nt as unknown, O as RECORDING_EXPORT_MAX_READ_BYTES, Ot as number, P as SceneMonitorSchema, Pt as EventCategory, Q as encodeVectorBase64, R as TimelapseRuleSchema, S as NcSnoozeInputSchema, St as sleep, T as NcSystemEventKindSchema, Tt as boolean, U as assertTimelapseCadences, V as addonWidgetsSourceCapability, W as audioMetricsCapability, X as deriveRecordingMode, Y as defineCustomActions, _ as NcRuleInputSchema, _t as DeviceType, at as pipelineAnalyticsCapability, b as NcRuleTargetSchema, bt as isDeviceScopedCap, c as EVENT_PAD_MS, ct as readTimelapseGeneratedAt, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as systemEventFilterApplies, f as NC_CONDITION_CATALOG, ft as vectorDimFromBase64, g as NcConditionDescriptorSchema, gt as BaseAddon, h as NC_TAXONOMY, ht as errMsg, i as DETECTION_MACRO_CLASSES, it as notificationRulesCapability, j as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as record, k as RetrainStatusSchema, kt as object, l as LabelAttributionSchema, lt as sceneMonitorCapability, mt as zoneAnalyticsCapability, n as DEFAULT_EVENT_COLOR, nt as isScheduleActive, o as DeclaredDevices, ot as plateGalleryCapability, p as NC_DEFAULT_SNOOZE_MINUTES, pt as videoclipsCapability, q as cosineSimilarity$1, rt as kebabToCamel, s as EVENT_KIND_BY_CAP, st as readDeviceStateFrom, t as BaseDevice, tt as isDetectionMacroClass, u as MACRO_LABELS, ut as subKindsOf, v as NcRulePatchSchema, vt as createEvent, w as NcSnoozeSuppressedSchema, wt as array, x as NcScheduleSchema, xt as nodePin, y as NcRuleSchema, yt as hydrateSchema, z as TrackSourceSchema } from "../dist-VnaTfwAL.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";
|
|
@@ -28833,51 +28833,6 @@ var PlateGalleryProvider = class {
|
|
|
28833
28833
|
};
|
|
28834
28834
|
}
|
|
28835
28835
|
};
|
|
28836
|
-
var OrphanAuditActionInputSchema = object({
|
|
28837
|
-
mode: _enum(["report", "reclaim"]).default("report"),
|
|
28838
|
-
/**
|
|
28839
|
-
* Start every scope from the top instead of resuming its persisted cursor.
|
|
28840
|
-
*
|
|
28841
|
-
* What draining a backlog wants, and never the default — the cursor exists
|
|
28842
|
-
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
28843
|
-
*/
|
|
28844
|
-
restart: boolean().optional(),
|
|
28845
|
-
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
28846
|
-
pageSize: number().int().min(50).max(5e3).optional(),
|
|
28847
|
-
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
28848
|
-
maxRowsPerScope: number().int().min(1).max(2e5).optional(),
|
|
28849
|
-
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
28850
|
-
maxReclaimPerScope: number().int().min(1).max(1e5).optional(),
|
|
28851
|
-
/**
|
|
28852
|
-
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
28853
|
-
* exclusively save rows, never select them, which is what keeps this off
|
|
28854
|
-
* the one-age-keyed-sweep guard's books.
|
|
28855
|
-
*/
|
|
28856
|
-
graceMinutes: number().int().min(1).max(10080).optional()
|
|
28857
|
-
});
|
|
28858
|
-
var OrphanAuditScopeReportSchema = object({
|
|
28859
|
-
collection: string(),
|
|
28860
|
-
examined: number(),
|
|
28861
|
-
orphans: number(),
|
|
28862
|
-
reclaimed: number(),
|
|
28863
|
-
bytesReclaimed: number(),
|
|
28864
|
-
exempt: number(),
|
|
28865
|
-
withinGrace: number(),
|
|
28866
|
-
unresolvable: number(),
|
|
28867
|
-
stoppedBy: string(),
|
|
28868
|
-
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
28869
|
-
nextCursor: number()
|
|
28870
|
-
});
|
|
28871
|
-
var orphanAuditActions = defineCustomActions({ "retention.orphanAudit": customAction(OrphanAuditActionInputSchema, object({
|
|
28872
|
-
/** `false` when a pass was already in flight — nothing was done. */
|
|
28873
|
-
ran: boolean(),
|
|
28874
|
-
mode: _enum(["report", "reclaim"]),
|
|
28875
|
-
scopes: array(OrphanAuditScopeReportSchema),
|
|
28876
|
-
totalOrphans: number(),
|
|
28877
|
-
totalReclaimed: number(),
|
|
28878
|
-
totalBytesReclaimed: number(),
|
|
28879
|
-
durationMs: number()
|
|
28880
|
-
}), { auth: "admin" }) });
|
|
28881
28836
|
//#endregion
|
|
28882
28837
|
//#region src/pipeline-analytics/store/storage-pressure.ts
|
|
28883
28838
|
/**
|
|
@@ -29045,6 +29000,11 @@ var FACE_MEDIA_OWNER_PREFIX = "face-";
|
|
|
29045
29000
|
* `plate-<trackId>`. Used by `deleteByTracks` to derive the plate crop owners
|
|
29046
29001
|
* of a set of tracks without a `trackId` column on media rows. */
|
|
29047
29002
|
var PLATE_MEDIA_OWNER_PREFIX = "plate-";
|
|
29003
|
+
/** Index rows per page inside one presence chunk. Must stay under
|
|
29004
|
+
* `SETTINGS_QUERY_DEFAULT_ROW_CAP` (2 000) so the engine's backstop can never
|
|
29005
|
+
* silently truncate a page — an owner reported absent because a cap bit would
|
|
29006
|
+
* DELETE a live thumbnail from the surface. */
|
|
29007
|
+
var MEDIA_PRESENCE_PAGE_SIZE = 1e3;
|
|
29048
29008
|
/**
|
|
29049
29009
|
* Kinds that hold exactly ONE row per `(ownerKind, ownerId)` — the current
|
|
29050
29010
|
* "best"/rolling/first artefact, never a filmstrip. Their row id + blob path are
|
|
@@ -29472,6 +29432,59 @@ var MediaStore = class {
|
|
|
29472
29432
|
});
|
|
29473
29433
|
}
|
|
29474
29434
|
/**
|
|
29435
|
+
* Which of `ownerIds` hold at least ONE media row — index only, no blob is
|
|
29436
|
+
* opened, no base64 is allocated.
|
|
29437
|
+
*
|
|
29438
|
+
* This is the licence `videoclips` needs to publish a `thumbnail`. The
|
|
29439
|
+
* event-media URL is stamped from a template for every event in the system,
|
|
29440
|
+
* so "the plane is up" was being served as "there are bytes": 64 of one
|
|
29441
|
+
* camera's 179 clips pointed at a 404 on 2026-08-17.
|
|
29442
|
+
*
|
|
29443
|
+
* Batched by `whereIn` and paged by offset. NOT `listInfoByOwner` in a loop:
|
|
29444
|
+
* that is one query per owner, which is the N+1 this exists to prevent (and
|
|
29445
|
+
* `queryObjectByTrackIds` already records what believing `whereIn` was
|
|
29446
|
+
* unavailable cost the search path).
|
|
29447
|
+
*
|
|
29448
|
+
* `deviceId` is the AUTHORIZATION filter, exactly as in `listByOwner`.
|
|
29449
|
+
*
|
|
29450
|
+
* THROWS on a query failure rather than answering partially — a probe that
|
|
29451
|
+
* quietly under-reports reads as "these owners have nothing", and the caller
|
|
29452
|
+
* drops thumbnails that exist.
|
|
29453
|
+
*/
|
|
29454
|
+
async ownersWithMedia(ownerKind, ownerIds, deviceId) {
|
|
29455
|
+
const found = /* @__PURE__ */ new Set();
|
|
29456
|
+
const unique = [...new Set(ownerIds)];
|
|
29457
|
+
for (let i = 0; i < unique.length; i += 200) {
|
|
29458
|
+
const chunk = unique.slice(i, i + 200);
|
|
29459
|
+
const answered = /* @__PURE__ */ new Set();
|
|
29460
|
+
for (let offset = 0;; offset += MEDIA_PRESENCE_PAGE_SIZE) {
|
|
29461
|
+
const rows = await this.store.query.query({
|
|
29462
|
+
collection: MEDIA_COLLECTION,
|
|
29463
|
+
filter: {
|
|
29464
|
+
where: {
|
|
29465
|
+
ownerKind,
|
|
29466
|
+
deviceId
|
|
29467
|
+
},
|
|
29468
|
+
whereIn: { ownerId: chunk },
|
|
29469
|
+
orderBy: {
|
|
29470
|
+
field: "ownerId",
|
|
29471
|
+
direction: "asc"
|
|
29472
|
+
},
|
|
29473
|
+
limit: MEDIA_PRESENCE_PAGE_SIZE,
|
|
29474
|
+
offset
|
|
29475
|
+
}
|
|
29476
|
+
});
|
|
29477
|
+
for (const row of rows) {
|
|
29478
|
+
const ownerId = row.data["ownerId"];
|
|
29479
|
+
if (typeof ownerId === "string") answered.add(ownerId);
|
|
29480
|
+
}
|
|
29481
|
+
if (rows.length < 1e3 || answered.size >= chunk.length) break;
|
|
29482
|
+
}
|
|
29483
|
+
for (const ownerId of answered) found.add(ownerId);
|
|
29484
|
+
}
|
|
29485
|
+
return found;
|
|
29486
|
+
}
|
|
29487
|
+
/**
|
|
29475
29488
|
* Delete all media (blobs + index rows) for the given owners of one kind.
|
|
29476
29489
|
* Best-effort per row; returns the number of index rows removed.
|
|
29477
29490
|
*/
|
|
@@ -29818,6 +29831,51 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
29818
29831
|
events: doomedEvents.length
|
|
29819
29832
|
};
|
|
29820
29833
|
}
|
|
29834
|
+
var OrphanAuditActionInputSchema = object({
|
|
29835
|
+
mode: _enum(["report", "reclaim"]).default("report"),
|
|
29836
|
+
/**
|
|
29837
|
+
* Start every scope from the top instead of resuming its persisted cursor.
|
|
29838
|
+
*
|
|
29839
|
+
* What draining a backlog wants, and never the default — the cursor exists
|
|
29840
|
+
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
29841
|
+
*/
|
|
29842
|
+
restart: boolean().optional(),
|
|
29843
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
29844
|
+
pageSize: number().int().min(50).max(5e3).optional(),
|
|
29845
|
+
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
29846
|
+
maxRowsPerScope: number().int().min(1).max(2e5).optional(),
|
|
29847
|
+
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
29848
|
+
maxReclaimPerScope: number().int().min(1).max(1e5).optional(),
|
|
29849
|
+
/**
|
|
29850
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
29851
|
+
* exclusively save rows, never select them, which is what keeps this off
|
|
29852
|
+
* the one-age-keyed-sweep guard's books.
|
|
29853
|
+
*/
|
|
29854
|
+
graceMinutes: number().int().min(1).max(10080).optional()
|
|
29855
|
+
});
|
|
29856
|
+
var OrphanAuditScopeReportSchema = object({
|
|
29857
|
+
collection: string(),
|
|
29858
|
+
examined: number(),
|
|
29859
|
+
orphans: number(),
|
|
29860
|
+
reclaimed: number(),
|
|
29861
|
+
bytesReclaimed: number(),
|
|
29862
|
+
exempt: number(),
|
|
29863
|
+
withinGrace: number(),
|
|
29864
|
+
unresolvable: number(),
|
|
29865
|
+
stoppedBy: string(),
|
|
29866
|
+
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
29867
|
+
nextCursor: number()
|
|
29868
|
+
});
|
|
29869
|
+
var orphanAuditActions = defineCustomActions({ "retention.orphanAudit": customAction(OrphanAuditActionInputSchema, object({
|
|
29870
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
29871
|
+
ran: boolean(),
|
|
29872
|
+
mode: _enum(["report", "reclaim"]),
|
|
29873
|
+
scopes: array(OrphanAuditScopeReportSchema),
|
|
29874
|
+
totalOrphans: number(),
|
|
29875
|
+
totalReclaimed: number(),
|
|
29876
|
+
totalBytesReclaimed: number(),
|
|
29877
|
+
durationMs: number()
|
|
29878
|
+
}), { auth: "admin" }) });
|
|
29821
29879
|
//#endregion
|
|
29822
29880
|
//#region src/pipeline-analytics/videoclip-visits.ts
|
|
29823
29881
|
/**
|
|
@@ -29885,7 +29943,22 @@ function dominantKind(attached) {
|
|
|
29885
29943
|
for (const kind of KIND_ORDER) if (attached.some((a) => a.kind === kind)) return kind;
|
|
29886
29944
|
return "motion";
|
|
29887
29945
|
}
|
|
29888
|
-
|
|
29946
|
+
/**
|
|
29947
|
+
* The instant a recorded still can actually be decoded inside a visit: the
|
|
29948
|
+
* midpoint of its LONGEST contiguous range.
|
|
29949
|
+
*
|
|
29950
|
+
* NOT the midpoint of the visit. A visit spans its holes by construction (it is
|
|
29951
|
+
* ranges glued across gaps up to `VISIT_MERGE_GAP_MS`), so the naive midpoint
|
|
29952
|
+
* lands in dead air roughly as often as not and the recorder's still route
|
|
29953
|
+
* answers 204 for a visit with plenty of footage.
|
|
29954
|
+
*/
|
|
29955
|
+
function stillInstantMs(ranges) {
|
|
29956
|
+
let best;
|
|
29957
|
+
for (const range of ranges) if (best === void 0 || range.endMs - range.startMs > best.endMs - best.startMs) best = range;
|
|
29958
|
+
if (best === void 0) return void 0;
|
|
29959
|
+
return Math.floor((best.startMs + best.endMs) / 2);
|
|
29960
|
+
}
|
|
29961
|
+
function visitsToClips(visits, events, pad, vouched) {
|
|
29889
29962
|
const tagged = [
|
|
29890
29963
|
...events.object.map((event) => ({
|
|
29891
29964
|
kind: "object",
|
|
@@ -29903,7 +29976,8 @@ function visitsToClips(visits, events, pad) {
|
|
|
29903
29976
|
return visits.map((visit) => {
|
|
29904
29977
|
const attached = tagged.filter((t) => eventOverlapsVisit(t.event, visit, pad));
|
|
29905
29978
|
const kind = dominantKind(attached);
|
|
29906
|
-
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl) ?? attached.find((a) => a.event.mediaUrl);
|
|
29979
|
+
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl && vouched.has(a.event.id)) ?? attached.find((a) => a.event.mediaUrl && vouched.has(a.event.id));
|
|
29980
|
+
const stillAtMs = stillInstantMs(visit.ranges);
|
|
29907
29981
|
return {
|
|
29908
29982
|
id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
|
|
29909
29983
|
source: "analytics",
|
|
@@ -29912,8 +29986,10 @@ function visitsToClips(visits, events, pad) {
|
|
|
29912
29986
|
startMs: visit.startMs,
|
|
29913
29987
|
endMs: visit.endMs
|
|
29914
29988
|
},
|
|
29915
|
-
thumbnail: withThumb
|
|
29916
|
-
|
|
29989
|
+
...withThumb?.event.mediaUrl !== void 0 ? { thumbnail: withThumb.event.mediaUrl } : {},
|
|
29990
|
+
...stillAtMs !== void 0 ? { stillAtMs } : {},
|
|
29991
|
+
eventIds: attached.slice(0, 24).map((a) => a.event.id),
|
|
29992
|
+
eventCount: attached.length,
|
|
29917
29993
|
holes: [...visit.holes]
|
|
29918
29994
|
};
|
|
29919
29995
|
}).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
@@ -29937,6 +30013,17 @@ function visitsToClips(visits, events, pad) {
|
|
|
29937
30013
|
* injected fetch/resolve deps so the compose logic is testable without I/O.
|
|
29938
30014
|
*/
|
|
29939
30015
|
var SOURCE = "analytics";
|
|
30016
|
+
/**
|
|
30017
|
+
* Backstop on the ANNOTATION read — never on the clip list.
|
|
30018
|
+
*
|
|
30019
|
+
* A visit is annotated by every event inside it, so the clip `limit` must not
|
|
30020
|
+
* reach this fetch (one 615 visit legitimately spans 2 345 motion rows). This
|
|
30021
|
+
* bound exists only so a pathological camera-day cannot pull an unbounded page:
|
|
30022
|
+
* the store orders `timestamp DESC`, so a truncation drops the OLDEST rows and
|
|
30023
|
+
* the EARLIEST visits silently lose their annotations — which is why hitting it
|
|
30024
|
+
* is logged, per camera, rather than absorbed.
|
|
30025
|
+
*/
|
|
30026
|
+
var VISIT_EVENT_FETCH_LIMIT = 5e3;
|
|
29940
30027
|
function clipIdFor(eventId, startMs, endMs) {
|
|
29941
30028
|
return `${SOURCE}:${startMs}:${endMs}:${eventId}`;
|
|
29942
30029
|
}
|
|
@@ -29957,7 +30044,7 @@ function parseClipId(clipId) {
|
|
|
29957
30044
|
};
|
|
29958
30045
|
}
|
|
29959
30046
|
/** Map per-kind events into time-ranged clips, newest-first. Pure. */
|
|
29960
|
-
function eventsToClips(events, pad) {
|
|
30047
|
+
function eventsToClips(events, pad, vouched) {
|
|
29961
30048
|
const make = (e, kind) => {
|
|
29962
30049
|
const startMs = e.timestamp - pad.preMs;
|
|
29963
30050
|
const endMs = e.timestamp + pad.postMs;
|
|
@@ -29969,7 +30056,7 @@ function eventsToClips(events, pad) {
|
|
|
29969
30056
|
startMs,
|
|
29970
30057
|
endMs
|
|
29971
30058
|
},
|
|
29972
|
-
thumbnail: e.mediaUrl
|
|
30059
|
+
...e.mediaUrl !== void 0 && vouched.has(e.id) ? { thumbnail: e.mediaUrl } : {}
|
|
29973
30060
|
};
|
|
29974
30061
|
};
|
|
29975
30062
|
return [
|
|
@@ -29978,29 +30065,106 @@ function eventsToClips(events, pad) {
|
|
|
29978
30065
|
...events.audio.map((e) => make(e, "audio"))
|
|
29979
30066
|
].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
29980
30067
|
}
|
|
30068
|
+
/** Every id in the three buckets, in the order the composers tag them. */
|
|
30069
|
+
function allEventIds(events) {
|
|
30070
|
+
return [
|
|
30071
|
+
...events.motion,
|
|
30072
|
+
...events.object,
|
|
30073
|
+
...events.audio
|
|
30074
|
+
].map((e) => e.id);
|
|
30075
|
+
}
|
|
30076
|
+
/**
|
|
30077
|
+
* Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
|
|
30078
|
+
* `resolveDefaultEventMedia`: the event's OWN media when it has any, else its
|
|
30079
|
+
* owning TRACK's. Two batched index reads at most, and the second only over the
|
|
30080
|
+
* object events the first did not already answer for — motion and audio are
|
|
30081
|
+
* trackless roots, so they never need it.
|
|
30082
|
+
*
|
|
30083
|
+
* THROWS on a failed read. The caller fails closed on the CLAIM and open on the
|
|
30084
|
+
* LIST; swallowing here would collapse those two opposite directions into one.
|
|
30085
|
+
*/
|
|
30086
|
+
async function resolveVouchedEvents(deviceId, events, fetchMediaOwners) {
|
|
30087
|
+
const eventIds = allEventIds(events);
|
|
30088
|
+
if (eventIds.length === 0) return /* @__PURE__ */ new Set();
|
|
30089
|
+
const ownMedia = await fetchMediaOwners({
|
|
30090
|
+
deviceId,
|
|
30091
|
+
ownerKind: "event",
|
|
30092
|
+
ownerIds: eventIds
|
|
30093
|
+
});
|
|
30094
|
+
const residualTracks = [];
|
|
30095
|
+
const seenTracks = /* @__PURE__ */ new Set();
|
|
30096
|
+
for (const event of events.object) {
|
|
30097
|
+
const trackId = event.trackId;
|
|
30098
|
+
if (trackId === void 0 || ownMedia.has(event.id) || seenTracks.has(trackId)) continue;
|
|
30099
|
+
seenTracks.add(trackId);
|
|
30100
|
+
residualTracks.push(trackId);
|
|
30101
|
+
}
|
|
30102
|
+
const trackMedia = residualTracks.length === 0 ? /* @__PURE__ */ new Set() : await fetchMediaOwners({
|
|
30103
|
+
deviceId,
|
|
30104
|
+
ownerKind: "track",
|
|
30105
|
+
ownerIds: residualTracks
|
|
30106
|
+
});
|
|
30107
|
+
const vouched = new Set(ownMedia);
|
|
30108
|
+
for (const event of events.object) {
|
|
30109
|
+
const trackId = event.trackId;
|
|
30110
|
+
if (trackId !== void 0 && trackMedia.has(trackId)) vouched.add(event.id);
|
|
30111
|
+
}
|
|
30112
|
+
return vouched;
|
|
30113
|
+
}
|
|
30114
|
+
/** Keep only clips that actually touch the caller's window. The reads are
|
|
30115
|
+
* widened by {@link leftOverhangMs}; the ANSWER is not. */
|
|
30116
|
+
function intersectsWindow(clip, since, until) {
|
|
30117
|
+
return clip.timeRange.startMs < until && clip.timeRange.endMs > since;
|
|
30118
|
+
}
|
|
30119
|
+
/**
|
|
30120
|
+
* How far BEFORE `since` the event + availability reads must reach.
|
|
30121
|
+
*
|
|
30122
|
+
* A visit straddling `since` keeps its heartbeat rows and loses the transition
|
|
30123
|
+
* row — the only one that ever carries a snapshot — so the same visit shows a
|
|
30124
|
+
* picture or doesn't depending on where the operator scrolled. One wider range
|
|
30125
|
+
* scan on an indexed column buys a list that stops changing under the cursor.
|
|
30126
|
+
*/
|
|
30127
|
+
function leftOverhangMs(pad) {
|
|
30128
|
+
return VISIT_MERGE_GAP_MS + pad.preMs;
|
|
30129
|
+
}
|
|
29981
30130
|
function createVideoclipsProvider(deps) {
|
|
29982
30131
|
return {
|
|
29983
30132
|
listClips: async ({ deviceId, since, until, limit }) => {
|
|
30133
|
+
const readSince = since - leftOverhangMs(deps.defaultPad);
|
|
29984
30134
|
const events = await deps.fetchEvents({
|
|
29985
30135
|
deviceId,
|
|
29986
|
-
since,
|
|
30136
|
+
since: readSince,
|
|
29987
30137
|
until,
|
|
29988
|
-
|
|
30138
|
+
limit: deps.fetchAvailability ? VISIT_EVENT_FETCH_LIMIT : limit
|
|
29989
30139
|
});
|
|
29990
|
-
|
|
30140
|
+
warnOnTruncatedFetch(deps, deviceId, events);
|
|
30141
|
+
let vouched = /* @__PURE__ */ new Set();
|
|
30142
|
+
try {
|
|
30143
|
+
vouched = await resolveVouchedEvents(deviceId, events, deps.fetchMediaOwners);
|
|
30144
|
+
} catch (err) {
|
|
30145
|
+
deps.logWarn?.("videoclips: media presence read failed — listing without thumbnails", {
|
|
30146
|
+
deviceId,
|
|
30147
|
+
events: allEventIds(events).length,
|
|
30148
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30149
|
+
});
|
|
30150
|
+
}
|
|
30151
|
+
let clips = eventsToClips(events, deps.defaultPad, vouched);
|
|
29991
30152
|
if (deps.fetchAvailability) try {
|
|
29992
30153
|
clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
|
|
29993
30154
|
deviceId,
|
|
29994
|
-
since,
|
|
30155
|
+
since: readSince,
|
|
29995
30156
|
until
|
|
29996
|
-
}), VISIT_MERGE_GAP_MS), events, deps.defaultPad);
|
|
30157
|
+
}), VISIT_MERGE_GAP_MS), events, deps.defaultPad, vouched);
|
|
29997
30158
|
} catch (err) {
|
|
29998
30159
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
29999
30160
|
deviceId,
|
|
30000
30161
|
error: err instanceof Error ? err.message : String(err)
|
|
30001
30162
|
});
|
|
30002
30163
|
}
|
|
30003
|
-
|
|
30164
|
+
const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
|
|
30165
|
+
const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
|
|
30166
|
+
reportUnvouchedClips(deps, deviceId, answer);
|
|
30167
|
+
return answer;
|
|
30004
30168
|
},
|
|
30005
30169
|
getClipPlayback: async ({ deviceId, clipId }) => {
|
|
30006
30170
|
const parsed = parseClipId(clipId);
|
|
@@ -30013,6 +30177,33 @@ function createVideoclipsProvider(deps) {
|
|
|
30013
30177
|
}
|
|
30014
30178
|
};
|
|
30015
30179
|
}
|
|
30180
|
+
/** The annotation backstop biting is a SILENT loss of the earliest visits'
|
|
30181
|
+
* events — say so, per camera, instead of shipping a thinner list. */
|
|
30182
|
+
function warnOnTruncatedFetch(deps, deviceId, events) {
|
|
30183
|
+
if (!deps.fetchAvailability) return;
|
|
30184
|
+
const truncated = [
|
|
30185
|
+
"motion",
|
|
30186
|
+
"object",
|
|
30187
|
+
"audio"
|
|
30188
|
+
].filter((kind) => events[kind].length >= VISIT_EVENT_FETCH_LIMIT);
|
|
30189
|
+
if (truncated.length === 0) return;
|
|
30190
|
+
deps.logWarn?.("videoclips: annotation fetch truncated — earliest visits lose their events", {
|
|
30191
|
+
deviceId,
|
|
30192
|
+
kinds: truncated,
|
|
30193
|
+
limit: VISIT_EVENT_FETCH_LIMIT
|
|
30194
|
+
});
|
|
30195
|
+
}
|
|
30196
|
+
/** The operator's acceptance criterion as a number: a rising count on one
|
|
30197
|
+
* camera is its motion-snapshot capturer failing, not a quiet day. */
|
|
30198
|
+
function reportUnvouchedClips(deps, deviceId, clips) {
|
|
30199
|
+
const clipsWithoutStill = clips.filter((c) => c.thumbnail === void 0).length;
|
|
30200
|
+
if (clipsWithoutStill === 0) return;
|
|
30201
|
+
deps.logInfo?.("videoclips: visit has no vouched still", {
|
|
30202
|
+
deviceId,
|
|
30203
|
+
clipsWithoutStill,
|
|
30204
|
+
clips: clips.length
|
|
30205
|
+
});
|
|
30206
|
+
}
|
|
30016
30207
|
//#endregion
|
|
30017
30208
|
//#region src/pipeline-analytics/viewer-settings-actions.ts
|
|
30018
30209
|
/**
|
|
@@ -47202,6 +47393,66 @@ var EventMediaDispatcher = class {
|
|
|
47202
47393
|
}
|
|
47203
47394
|
}
|
|
47204
47395
|
};
|
|
47396
|
+
var SnapshotTimeoutError$1 = class extends Error {
|
|
47397
|
+
constructor(ms) {
|
|
47398
|
+
super(`snapshot cap timed out after ${ms}ms`);
|
|
47399
|
+
this.name = "SnapshotTimeoutError";
|
|
47400
|
+
}
|
|
47401
|
+
};
|
|
47402
|
+
function withTimeout$1(promise, ms) {
|
|
47403
|
+
return new Promise((resolve, reject) => {
|
|
47404
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError$1(ms)), ms);
|
|
47405
|
+
promise.then((value) => {
|
|
47406
|
+
clearTimeout(timer);
|
|
47407
|
+
resolve(value);
|
|
47408
|
+
}, (err) => {
|
|
47409
|
+
clearTimeout(timer);
|
|
47410
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47411
|
+
});
|
|
47412
|
+
});
|
|
47413
|
+
}
|
|
47414
|
+
var MotionEventSnapshotCapturer = class {
|
|
47415
|
+
deps;
|
|
47416
|
+
snapshotTimeoutMs;
|
|
47417
|
+
constructor(deps) {
|
|
47418
|
+
this.deps = deps;
|
|
47419
|
+
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
47420
|
+
}
|
|
47421
|
+
/**
|
|
47422
|
+
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
47423
|
+
* throws — the durable record is the motion row, already inserted by
|
|
47424
|
+
* the caller.
|
|
47425
|
+
*/
|
|
47426
|
+
async capture(input) {
|
|
47427
|
+
let snap;
|
|
47428
|
+
try {
|
|
47429
|
+
snap = await withTimeout$1(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
47430
|
+
} catch (err) {
|
|
47431
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError$1 ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
47432
|
+
return;
|
|
47433
|
+
}
|
|
47434
|
+
if (snap === null) return;
|
|
47435
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
47436
|
+
let data = raw;
|
|
47437
|
+
try {
|
|
47438
|
+
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
47439
|
+
} catch (err) {
|
|
47440
|
+
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
47441
|
+
}
|
|
47442
|
+
try {
|
|
47443
|
+
await this.deps.media.put({
|
|
47444
|
+
deviceId: input.deviceId,
|
|
47445
|
+
ownerKind: "event",
|
|
47446
|
+
ownerId: input.eventId,
|
|
47447
|
+
kind: "snapshot",
|
|
47448
|
+
timestamp: input.timestamp,
|
|
47449
|
+
data
|
|
47450
|
+
});
|
|
47451
|
+
} catch (err) {
|
|
47452
|
+
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
47453
|
+
}
|
|
47454
|
+
}
|
|
47455
|
+
};
|
|
47205
47456
|
/**
|
|
47206
47457
|
* Project one `DeviceStateChanged` of a mapped sensor/control cap into a
|
|
47207
47458
|
* synthetic track per camera the event belongs to. Returns the number of
|
|
@@ -47298,16 +47549,16 @@ var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
|
47298
47549
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
47299
47550
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
47300
47551
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
47301
|
-
var SnapshotTimeoutError
|
|
47552
|
+
var SnapshotTimeoutError = class extends Error {
|
|
47302
47553
|
constructor(ms) {
|
|
47303
47554
|
super(`snapshot cap timed out after ${ms}ms`);
|
|
47304
47555
|
this.name = "SnapshotTimeoutError";
|
|
47305
47556
|
}
|
|
47306
47557
|
};
|
|
47307
47558
|
/** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
|
|
47308
|
-
function withTimeout
|
|
47559
|
+
function withTimeout(promise, ms) {
|
|
47309
47560
|
return new Promise((resolve, reject) => {
|
|
47310
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError
|
|
47561
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
47311
47562
|
promise.then((value) => {
|
|
47312
47563
|
clearTimeout(timer);
|
|
47313
47564
|
resolve(value);
|
|
@@ -47362,11 +47613,11 @@ var SyntheticTrackMaterializer = class {
|
|
|
47362
47613
|
*/
|
|
47363
47614
|
async fetchSnapshot(cameraId, timeoutMs) {
|
|
47364
47615
|
try {
|
|
47365
|
-
const snap = await withTimeout
|
|
47616
|
+
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
47366
47617
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"), cameraId);
|
|
47367
47618
|
return snap;
|
|
47368
47619
|
} catch (err) {
|
|
47369
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError
|
|
47620
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
47370
47621
|
return null;
|
|
47371
47622
|
}
|
|
47372
47623
|
}
|
|
@@ -47432,66 +47683,6 @@ var SyntheticTrackMaterializer = class {
|
|
|
47432
47683
|
return track;
|
|
47433
47684
|
}
|
|
47434
47685
|
};
|
|
47435
|
-
var SnapshotTimeoutError = class extends Error {
|
|
47436
|
-
constructor(ms) {
|
|
47437
|
-
super(`snapshot cap timed out after ${ms}ms`);
|
|
47438
|
-
this.name = "SnapshotTimeoutError";
|
|
47439
|
-
}
|
|
47440
|
-
};
|
|
47441
|
-
function withTimeout(promise, ms) {
|
|
47442
|
-
return new Promise((resolve, reject) => {
|
|
47443
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
47444
|
-
promise.then((value) => {
|
|
47445
|
-
clearTimeout(timer);
|
|
47446
|
-
resolve(value);
|
|
47447
|
-
}, (err) => {
|
|
47448
|
-
clearTimeout(timer);
|
|
47449
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47450
|
-
});
|
|
47451
|
-
});
|
|
47452
|
-
}
|
|
47453
|
-
var MotionEventSnapshotCapturer = class {
|
|
47454
|
-
deps;
|
|
47455
|
-
snapshotTimeoutMs;
|
|
47456
|
-
constructor(deps) {
|
|
47457
|
-
this.deps = deps;
|
|
47458
|
-
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
47459
|
-
}
|
|
47460
|
-
/**
|
|
47461
|
-
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
47462
|
-
* throws — the durable record is the motion row, already inserted by
|
|
47463
|
-
* the caller.
|
|
47464
|
-
*/
|
|
47465
|
-
async capture(input) {
|
|
47466
|
-
let snap;
|
|
47467
|
-
try {
|
|
47468
|
-
snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
47469
|
-
} catch (err) {
|
|
47470
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
47471
|
-
return;
|
|
47472
|
-
}
|
|
47473
|
-
if (snap === null) return;
|
|
47474
|
-
const raw = Buffer.from(snap.base64, "base64");
|
|
47475
|
-
let data = raw;
|
|
47476
|
-
try {
|
|
47477
|
-
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
47478
|
-
} catch (err) {
|
|
47479
|
-
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
47480
|
-
}
|
|
47481
|
-
try {
|
|
47482
|
-
await this.deps.media.put({
|
|
47483
|
-
deviceId: input.deviceId,
|
|
47484
|
-
ownerKind: "event",
|
|
47485
|
-
ownerId: input.eventId,
|
|
47486
|
-
kind: "snapshot",
|
|
47487
|
-
timestamp: input.timestamp,
|
|
47488
|
-
data
|
|
47489
|
-
});
|
|
47490
|
-
} catch (err) {
|
|
47491
|
-
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
47492
|
-
}
|
|
47493
|
-
}
|
|
47494
|
-
};
|
|
47495
47686
|
var TrackingSettingsSchema = object({
|
|
47496
47687
|
/**
|
|
47497
47688
|
* How much of a detection's box must lie inside a zone (0-1 fraction of the
|
|
@@ -53031,6 +53222,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53031
53222
|
audio
|
|
53032
53223
|
};
|
|
53033
53224
|
},
|
|
53225
|
+
fetchMediaOwners: async ({ deviceId, ownerKind, ownerIds }) => {
|
|
53226
|
+
return stores.mediaStore.ownersWithMedia(ownerKind, ownerIds, deviceId);
|
|
53227
|
+
},
|
|
53034
53228
|
fetchAvailability: async ({ deviceId, since, until }) => {
|
|
53035
53229
|
return (await api.recording.getAvailability.query({
|
|
53036
53230
|
deviceId,
|
|
@@ -53042,6 +53236,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53042
53236
|
tags: { deviceId: Number(meta["deviceId"]) },
|
|
53043
53237
|
meta
|
|
53044
53238
|
}),
|
|
53239
|
+
logInfo: (message, meta) => this.ctx.logger.info(message, {
|
|
53240
|
+
tags: { deviceId: Number(meta["deviceId"]) },
|
|
53241
|
+
meta
|
|
53242
|
+
}),
|
|
53045
53243
|
resolvePlayback: async ({ deviceId, startMs, endMs }) => {
|
|
53046
53244
|
const manifest = await api.recording.getPlaybackManifest.query({
|
|
53047
53245
|
deviceId,
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BIDGLXRs.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.92",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|