@camstack/addon-post-analysis 1.2.231 → 1.2.233
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-DAXBbWmc.js → dist-Dbf6ceXh.js} +106 -5
- package/dist/{dist-QjlwKSkb.mjs → dist-yyQ1H5yE.mjs} +106 -5
- 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-C5_YZ4mw.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BvU7JaBW.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-yat2fBJg.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C5YuStaN.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BJyNnbrv.mjs → hostInit-Dg-LBlut.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +556 -41
- package/dist/pipeline-analytics/index.mjs +556 -41
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as plateGalleryCapability, B as NcTaxonomySchema, Bt as videoclipsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as isSourceCap, D as NC_DEFAULT_SNOOZE_MINUTES, E as NC_CONDITION_CATALOG, Et as notificationRulesCapability, F as NcScheduleSchema, Ft as sceneMonitorCapability, Gt as DeviceType, Ht as errMsg$1, I as NcSnoozeInputSchema, It as storageOccupancyCapability, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as createEvent, L as NcSnoozeSchema, Lt as subKindsOf, M as NcRulePatchSchema, Mt as readTimelapseGeneratedAt, N as NcRuleSchema, Nt as resolveLocationMode, Ot as pickClusterStepModels, P as NcRuleTargetSchema, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as systemEventFilterApplies, St as isScheduleActive, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as mayWriteToLocation, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as OpsLogEntrySchema, Vt as zoneAnalyticsCapability, W as RetrainStatusSchema, Wt as CamProfileSchema, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as failureContributionCapability, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isEventOwnerType, cn as EventCategory, ct as cosineSimilarity$1, dt as deriveRecordingMode, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, g as FIRST_LEVEL_MACRO_CLASSES, gt as faceGalleryCapability, h as EVENT_PAD_MS, ht as evictionPolicyOfLocation, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readDeviceStateFrom, k as NC_TAXONOMY, kt as pipelineAnalyticsCapability, lt as customAction, m as EVENT_OWNER_TYPES, mt as evaluateSensorEdge, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as encodeVectorBase64, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, ut as defineCustomActions, v as FailureCounters, w as MediaFileKindEnum, wt as kebabToCamel, x as MAX_BIRTH_DECISION_RECORDS, xt as isMediaOwnerType, y as LabelAttributionSchema, yt as isDetectionMacroClass, z as NcSystemEventKindSchema, zt as vectorDimFromBase64 } from "../dist-
|
|
1
|
+
import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as plateGalleryCapability, B as NcTaxonomySchema, Bt as videoclipsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as isSourceCap, D as NC_DEFAULT_SNOOZE_MINUTES, E as NC_CONDITION_CATALOG, Et as notificationRulesCapability, F as NcScheduleSchema, Ft as sceneMonitorCapability, Gt as DeviceType, Ht as errMsg$1, I as NcSnoozeInputSchema, It as storageOccupancyCapability, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as createEvent, L as NcSnoozeSchema, Lt as subKindsOf, M as NcRulePatchSchema, Mt as readTimelapseGeneratedAt, N as NcRuleSchema, Nt as resolveLocationMode, Ot as pickClusterStepModels, P as NcRuleTargetSchema, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as systemEventFilterApplies, St as isScheduleActive, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as mayWriteToLocation, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as OpsLogEntrySchema, Vt as zoneAnalyticsCapability, W as RetrainStatusSchema, Wt as CamProfileSchema, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as failureContributionCapability, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isEventOwnerType, cn as EventCategory, ct as cosineSimilarity$1, dt as deriveRecordingMode, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, g as FIRST_LEVEL_MACRO_CLASSES, gt as faceGalleryCapability, h as EVENT_PAD_MS, ht as evictionPolicyOfLocation, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readDeviceStateFrom, k as NC_TAXONOMY, kt as pipelineAnalyticsCapability, lt as customAction, m as EVENT_OWNER_TYPES, mt as evaluateSensorEdge, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as encodeVectorBase64, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, ut as defineCustomActions, v as FailureCounters, w as MediaFileKindEnum, wt as kebabToCamel, x as MAX_BIRTH_DECISION_RECORDS, xt as isMediaOwnerType, y as LabelAttributionSchema, yt as isDetectionMacroClass, z as NcSystemEventKindSchema, zt as vectorDimFromBase64 } from "../dist-yyQ1H5yE.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";
|
|
@@ -1965,8 +1965,8 @@ var NC_ARTIFACTS_INDEXES = [
|
|
|
1965
1965
|
}
|
|
1966
1966
|
];
|
|
1967
1967
|
/** Upper bound for a half-open `whereBetween` — the store has no `>=`. */
|
|
1968
|
-
var TIME_MAX = 864e13;
|
|
1969
|
-
var TIME_MIN = -864e13;
|
|
1968
|
+
var TIME_MAX$1 = 864e13;
|
|
1969
|
+
var TIME_MIN$1 = -864e13;
|
|
1970
1970
|
/** Prune scan ceiling — the index is bounded by the shelf it mirrors (2000
|
|
1971
1971
|
* entries by default), so this only has to be comfortably above it. */
|
|
1972
1972
|
var NC_ARTIFACT_PRUNE_SCAN_LIMIT = 2e4;
|
|
@@ -2206,7 +2206,7 @@ var NcArtifactIndex = class {
|
|
|
2206
2206
|
try {
|
|
2207
2207
|
const expired = await this.store.deleteWhere.mutate({
|
|
2208
2208
|
collection: NC_ARTIFACTS_COLLECTION,
|
|
2209
|
-
filter: { whereBetween: { expiresAt: [TIME_MIN, this.now() - 1] } }
|
|
2209
|
+
filter: { whereBetween: { expiresAt: [TIME_MIN$1, this.now() - 1] } }
|
|
2210
2210
|
});
|
|
2211
2211
|
removed += expired.deleted;
|
|
2212
2212
|
if (keptIds === null) {
|
|
@@ -2248,8 +2248,8 @@ var NcArtifactIndex = class {
|
|
|
2248
2248
|
whereIn["kind"] = query.kinds !== void 0 && query.kinds.length > 0 ? [...query.kinds] : [...NC_GALLERY_ARTIFACT_KINDS];
|
|
2249
2249
|
if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
|
|
2250
2250
|
const whereBetween = {
|
|
2251
|
-
capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
|
|
2252
|
-
expiresAt: [this.now(), TIME_MAX]
|
|
2251
|
+
capturedAt: [query.fromMs ?? TIME_MIN$1, query.toMs ?? TIME_MAX$1],
|
|
2252
|
+
expiresAt: [this.now(), TIME_MAX$1]
|
|
2253
2253
|
};
|
|
2254
2254
|
return {
|
|
2255
2255
|
...query.ruleId !== void 0 ? { where: { ruleId: query.ruleId } } : {},
|
|
@@ -2274,6 +2274,23 @@ var NcArtifactIndex = class {
|
|
|
2274
2274
|
return out;
|
|
2275
2275
|
}
|
|
2276
2276
|
};
|
|
2277
|
+
/**
|
|
2278
|
+
* A sentence explaining why this node must not keep the shelf, or `null` when
|
|
2279
|
+
* it may. The sentence is the log line: an operator reading it should learn
|
|
2280
|
+
* WHICH node owns the shelf without opening the storage page.
|
|
2281
|
+
*/
|
|
2282
|
+
function artifactShelfRefusal(locations, localNodeId) {
|
|
2283
|
+
if (localNodeId === null || localNodeId.length === 0) return null;
|
|
2284
|
+
const here = localNodeId.includes("/") ? localNodeId.split("/")[0] ?? "" : localNodeId;
|
|
2285
|
+
if (here.length === 0) return null;
|
|
2286
|
+
for (const location of locations) {
|
|
2287
|
+
if (!location.id.startsWith("artifacts")) continue;
|
|
2288
|
+
const owner = location.nodeId;
|
|
2289
|
+
if (owner === void 0 || owner.length === 0 || owner === here) continue;
|
|
2290
|
+
return `the artefact shelf lives on node "${owner}" (location "${location.id}"), not on "${here}" — the path it resolves to is that node's disk, and creating it here would make a directory of the same name that nothing reads. Artefacts are produced, stored and served by "${owner}".`;
|
|
2291
|
+
}
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2277
2294
|
//#endregion
|
|
2278
2295
|
//#region ../types/dist/node.mjs
|
|
2279
2296
|
var node_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -18686,6 +18703,52 @@ function buildSummaryOutboxInputs(input) {
|
|
|
18686
18703
|
});
|
|
18687
18704
|
}
|
|
18688
18705
|
//#endregion
|
|
18706
|
+
//#region src/notification-center/summary/summary-group-collapse.ts
|
|
18707
|
+
function collapseToGroups(candidates, groups) {
|
|
18708
|
+
const claim = /* @__PURE__ */ new Map();
|
|
18709
|
+
for (const group of groups) for (const trackId of group.memberTrackIds) if (!claim.has(trackId)) claim.set(trackId, group);
|
|
18710
|
+
const survivorsByGroup = /* @__PURE__ */ new Map();
|
|
18711
|
+
for (const candidate of candidates) {
|
|
18712
|
+
const group = claim.get(candidate.trackId);
|
|
18713
|
+
if (group === void 0) continue;
|
|
18714
|
+
const bucket = survivorsByGroup.get(group.summaryId);
|
|
18715
|
+
if (bucket === void 0) survivorsByGroup.set(group.summaryId, [candidate]);
|
|
18716
|
+
else bucket.push(candidate);
|
|
18717
|
+
}
|
|
18718
|
+
const representative = /* @__PURE__ */ new Map();
|
|
18719
|
+
for (const [summaryId, survivors] of survivorsByGroup) {
|
|
18720
|
+
const group = groups.find((g) => g.summaryId === summaryId);
|
|
18721
|
+
if (group === void 0) continue;
|
|
18722
|
+
const chosen = survivors.find((c) => c.trackId === group.primaryTrackId) ?? survivors[0];
|
|
18723
|
+
if (chosen === void 0) continue;
|
|
18724
|
+
representative.set(summaryId, {
|
|
18725
|
+
...chosen,
|
|
18726
|
+
groupSize: survivors.length,
|
|
18727
|
+
...group.groupShotMediaId !== null ? { groupShotMediaId: group.groupShotMediaId } : {}
|
|
18728
|
+
});
|
|
18729
|
+
}
|
|
18730
|
+
const kept = [];
|
|
18731
|
+
let collapsed = 0;
|
|
18732
|
+
for (const candidate of candidates) {
|
|
18733
|
+
const group = claim.get(candidate.trackId);
|
|
18734
|
+
if (group === void 0) {
|
|
18735
|
+
kept.push(candidate);
|
|
18736
|
+
continue;
|
|
18737
|
+
}
|
|
18738
|
+
const chosen = representative.get(group.summaryId);
|
|
18739
|
+
if (chosen === void 0) {
|
|
18740
|
+
kept.push(candidate);
|
|
18741
|
+
continue;
|
|
18742
|
+
}
|
|
18743
|
+
if (chosen.trackId === candidate.trackId) kept.push(chosen);
|
|
18744
|
+
else collapsed += 1;
|
|
18745
|
+
}
|
|
18746
|
+
return {
|
|
18747
|
+
kept,
|
|
18748
|
+
collapsed
|
|
18749
|
+
};
|
|
18750
|
+
}
|
|
18751
|
+
//#endregion
|
|
18689
18752
|
//#region src/notification-center/summary/summary-selection.ts
|
|
18690
18753
|
/**
|
|
18691
18754
|
* Which tracks become tiles — filter, rank, and the per-camera quota.
|
|
@@ -19258,7 +19321,8 @@ var NcSummaryProducer = class {
|
|
|
19258
19321
|
try {
|
|
19259
19322
|
const candidates = await this.collectCandidates(rule, window);
|
|
19260
19323
|
const filtered = filterSummaryCandidates(candidates, rule.filters, this.deps.zoneOwner);
|
|
19261
|
-
const
|
|
19324
|
+
const grouped = await this.collapseIfGrouping(rule, window, filtered.kept);
|
|
19325
|
+
const selection = selectSummaryTiles(grouped.kept, {
|
|
19262
19326
|
maxTiles: rule.maxTiles,
|
|
19263
19327
|
rank: rule.rank
|
|
19264
19328
|
});
|
|
@@ -19287,7 +19351,7 @@ var NcSummaryProducer = class {
|
|
|
19287
19351
|
const tileLoad = await this.loadTiles({
|
|
19288
19352
|
rule,
|
|
19289
19353
|
preferred: selection.tiles,
|
|
19290
|
-
refill: remainingSummaryCandidates(
|
|
19354
|
+
refill: remainingSummaryCandidates(grouped.kept, selection.tiles, rule.rank),
|
|
19291
19355
|
maxTiles: rule.maxTiles
|
|
19292
19356
|
});
|
|
19293
19357
|
const loaded = tileLoad.tiles;
|
|
@@ -19328,7 +19392,7 @@ var NcSummaryProducer = class {
|
|
|
19328
19392
|
priority: rule.priority,
|
|
19329
19393
|
tileCount: loaded.length,
|
|
19330
19394
|
cameraCount: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
19331
|
-
matchedCount:
|
|
19395
|
+
matchedCount: grouped.kept.length,
|
|
19332
19396
|
detections,
|
|
19333
19397
|
...mosaic.ref !== null ? { mosaic: mosaic.ref } : {},
|
|
19334
19398
|
generatedAt: this.deps.now(),
|
|
@@ -19340,7 +19404,7 @@ var NcSummaryProducer = class {
|
|
|
19340
19404
|
this.deps.logger.info("summary produced", { meta: {
|
|
19341
19405
|
...meta,
|
|
19342
19406
|
scanned: candidates.length,
|
|
19343
|
-
matched:
|
|
19407
|
+
matched: grouped.kept.length,
|
|
19344
19408
|
droppedStationary: filtered.droppedStationary,
|
|
19345
19409
|
droppedUnconfirmed: filtered.droppedUnconfirmed,
|
|
19346
19410
|
unknownConfirmation: filtered.unknownConfirmation,
|
|
@@ -19363,7 +19427,7 @@ var NcSummaryProducer = class {
|
|
|
19363
19427
|
window,
|
|
19364
19428
|
tiles: loaded.length,
|
|
19365
19429
|
cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
19366
|
-
matched:
|
|
19430
|
+
matched: grouped.kept.length,
|
|
19367
19431
|
droppedStationary: filtered.droppedStationary,
|
|
19368
19432
|
enqueued,
|
|
19369
19433
|
...mosaic.bytes > 0 ? { mosaicBytes: mosaic.bytes } : {},
|
|
@@ -19384,6 +19448,54 @@ var NcSummaryProducer = class {
|
|
|
19384
19448
|
};
|
|
19385
19449
|
}
|
|
19386
19450
|
}
|
|
19451
|
+
/**
|
|
19452
|
+
* Fold each session summary's surviving members into one entry — or do
|
|
19453
|
+
* nothing at all, which is what `tracks` (the default) means.
|
|
19454
|
+
*
|
|
19455
|
+
* A failure to read the groups does NOT fail the digest: it falls back to
|
|
19456
|
+
* subjects and says so. A digest of four lines instead of one is a worse
|
|
19457
|
+
* reading, not a lost one, and losing the night's record over a store read
|
|
19458
|
+
* is the wrong trade.
|
|
19459
|
+
*/
|
|
19460
|
+
async collapseIfGrouping(rule, window, kept) {
|
|
19461
|
+
if (rule.subject !== "groups") return {
|
|
19462
|
+
kept,
|
|
19463
|
+
collapsed: 0
|
|
19464
|
+
};
|
|
19465
|
+
const listGroups = this.deps.listSummaryGroups;
|
|
19466
|
+
if (listGroups === void 0) {
|
|
19467
|
+
this.deps.logger.warn("summary rule asks for groups but this node cannot read them", { meta: { ruleId: rule.id } });
|
|
19468
|
+
return {
|
|
19469
|
+
kept,
|
|
19470
|
+
collapsed: 0
|
|
19471
|
+
};
|
|
19472
|
+
}
|
|
19473
|
+
const groups = [];
|
|
19474
|
+
for (const deviceId of rule.deviceIds) try {
|
|
19475
|
+
groups.push(...await listGroups({
|
|
19476
|
+
deviceId,
|
|
19477
|
+
sinceMs: window.startMs,
|
|
19478
|
+
untilMs: window.endMs
|
|
19479
|
+
}));
|
|
19480
|
+
} catch (err) {
|
|
19481
|
+
this.deps.logger.warn("summary could not read a camera’s groups — read as subjects", {
|
|
19482
|
+
tags: { deviceId },
|
|
19483
|
+
meta: {
|
|
19484
|
+
ruleId: rule.id,
|
|
19485
|
+
error: String(err)
|
|
19486
|
+
}
|
|
19487
|
+
});
|
|
19488
|
+
}
|
|
19489
|
+
const outcome = collapseToGroups(kept, groups);
|
|
19490
|
+
this.deps.logger.info("summary read as groups", { meta: {
|
|
19491
|
+
ruleId: rule.id,
|
|
19492
|
+
groups: groups.length,
|
|
19493
|
+
subjects: kept.length,
|
|
19494
|
+
lines: outcome.kept.length,
|
|
19495
|
+
collapsed: outcome.collapsed
|
|
19496
|
+
} });
|
|
19497
|
+
return outcome;
|
|
19498
|
+
}
|
|
19387
19499
|
/** Every camera's tracks in the window, capped per camera and counted. */
|
|
19388
19500
|
async collectCandidates(rule, window) {
|
|
19389
19501
|
const all = [];
|
|
@@ -19433,6 +19545,25 @@ var NcSummaryProducer = class {
|
|
|
19433
19545
|
seenIds.add(candidate.trackId);
|
|
19434
19546
|
const tags = { deviceId: candidate.deviceId };
|
|
19435
19547
|
try {
|
|
19548
|
+
const groupShot = candidate.groupShotMediaId;
|
|
19549
|
+
if (groupShot !== void 0) {
|
|
19550
|
+
const jpeg = await this.deps.readMedia(groupShot);
|
|
19551
|
+
if (jpeg !== null && jpeg.byteLength > 0) {
|
|
19552
|
+
tiles.push({
|
|
19553
|
+
candidate,
|
|
19554
|
+
jpeg
|
|
19555
|
+
});
|
|
19556
|
+
continue;
|
|
19557
|
+
}
|
|
19558
|
+
this.deps.logger.info("summary tile: group shot unreadable — using a member", {
|
|
19559
|
+
tags,
|
|
19560
|
+
meta: {
|
|
19561
|
+
ruleId: input.rule.id,
|
|
19562
|
+
trackId: candidate.trackId,
|
|
19563
|
+
mediaId: groupShot
|
|
19564
|
+
}
|
|
19565
|
+
});
|
|
19566
|
+
}
|
|
19436
19567
|
const rows = await this.deps.listMediaInfo(candidate.trackId);
|
|
19437
19568
|
const picked = pickSummaryMedia(rows, (candidate.firstSeen + candidate.lastSeen) / 2);
|
|
19438
19569
|
if (picked === null) {
|
|
@@ -19895,6 +20026,21 @@ var NcSummaryRuleInputSchema = object({
|
|
|
19895
20026
|
deviceIds: DeviceIdsField,
|
|
19896
20027
|
window: NcSummaryWindowSchema,
|
|
19897
20028
|
filters: NcSummaryFiltersSchema.optional(),
|
|
20029
|
+
/**
|
|
20030
|
+
* What ONE line of this digest is: a subject, or a group of subjects that
|
|
20031
|
+
* arrived together.
|
|
20032
|
+
*
|
|
20033
|
+
* `tracks` is what the digest has always done and stays the default — the
|
|
20034
|
+
* operator's instruction when groups were added was that the first behaviour
|
|
20035
|
+
* remains. `groups` collapses the members of each session summary to one
|
|
20036
|
+
* entry, so a family walking in is one line and not four
|
|
20037
|
+
* (`summary-group-collapse.ts`).
|
|
20038
|
+
*
|
|
20039
|
+
* `.default('tracks')` is safe here where `NcSummaryFiltersSchema` fields are
|
|
20040
|
+
* not: this is a field of the RULE, not of the partial `filters` object that
|
|
20041
|
+
* a patch materialises wholesale.
|
|
20042
|
+
*/
|
|
20043
|
+
subject: _enum(["tracks", "groups"]).default("tracks"),
|
|
19898
20044
|
maxTiles: MaxTilesField.default(12),
|
|
19899
20045
|
maxAiImages: MaxAiImagesField.default(6),
|
|
19900
20046
|
rank: NcSummaryRankSchema.default("importance"),
|
|
@@ -19929,6 +20075,7 @@ var NcSummaryRulePatchSchema = object({
|
|
|
19929
20075
|
maxTiles: MaxTilesField.optional(),
|
|
19930
20076
|
maxAiImages: MaxAiImagesField.optional(),
|
|
19931
20077
|
rank: NcSummaryRankSchema.optional(),
|
|
20078
|
+
subject: _enum(["tracks", "groups"]).optional(),
|
|
19932
20079
|
barrierSeconds: BarrierSecondsField.optional(),
|
|
19933
20080
|
targets: TargetsField.optional(),
|
|
19934
20081
|
template: NcSummaryTemplateSchema.nullable().optional(),
|
|
@@ -43115,6 +43262,10 @@ function trackColumnsFor(projection, needsPositions = false) {
|
|
|
43115
43262
|
* whatever order the engine felt like and a cursor that cannot resume.
|
|
43116
43263
|
*/
|
|
43117
43264
|
var RECENT_IDS_COLUMNS = ["lastSeen"];
|
|
43265
|
+
/** How many track ids one filter pass may collect per tier. Bounded for the
|
|
43266
|
+
* same reason every bulk read here is: an unbounded scan of this table is
|
|
43267
|
+
* what D447 measured holding the settings thread for seconds. */
|
|
43268
|
+
var TRACK_MATCH_SCAN_LIMIT = 2e4;
|
|
43118
43269
|
/** Spreadable `columns` for a store query — omitted entirely for `full`. */
|
|
43119
43270
|
function projectionArg(projection, needsPositions = false) {
|
|
43120
43271
|
const columns = trackColumnsFor(projection, needsPositions);
|
|
@@ -44202,6 +44353,42 @@ var TrackStore = class {
|
|
|
44202
44353
|
* current subject out from under it. The orphan audit's grace window would
|
|
44203
44354
|
* hide this for an hour and then stop hiding it.
|
|
44204
44355
|
*/
|
|
44356
|
+
/**
|
|
44357
|
+
* Track ids whose CLASS, sub-class or instance matches any of `terms`.
|
|
44358
|
+
*
|
|
44359
|
+
* The summaries' filter asks its question here rather than on a copy: a
|
|
44360
|
+
* label is a fact about the TRACK, it is decided while the track lives and
|
|
44361
|
+
* can still be re-decided afterwards (class voting, a late face match, an
|
|
44362
|
+
* operator enrolling a face and renaming that person). A copy stamped onto
|
|
44363
|
+
* the summary would answer with whatever was true at one instant and never
|
|
44364
|
+
* learn — which is the whole reason this lookup exists instead.
|
|
44365
|
+
*
|
|
44366
|
+
* Three queries because the filter compiler has no OR across fields, and one
|
|
44367
|
+
* term can be any of the three tiers: `animal` is a class, `dog` is a
|
|
44368
|
+
* sub-class, `Roberta` is an instance. Bounded, like every other bulk read
|
|
44369
|
+
* here.
|
|
44370
|
+
*/
|
|
44371
|
+
async trackIdsMatching(terms) {
|
|
44372
|
+
const wanted = terms.filter((t) => t.length > 0);
|
|
44373
|
+
if (wanted.length === 0) return [];
|
|
44374
|
+
const ids = /* @__PURE__ */ new Set();
|
|
44375
|
+
for (const field of [
|
|
44376
|
+
"className",
|
|
44377
|
+
"label",
|
|
44378
|
+
"subLabel"
|
|
44379
|
+
]) {
|
|
44380
|
+
const rows = await this.store.query.query({
|
|
44381
|
+
collection: TRACKS_COLLECTION,
|
|
44382
|
+
filter: {
|
|
44383
|
+
whereIn: { [field]: [...wanted] },
|
|
44384
|
+
limit: TRACK_MATCH_SCAN_LIMIT
|
|
44385
|
+
},
|
|
44386
|
+
columns: ["id"]
|
|
44387
|
+
});
|
|
44388
|
+
for (const row of rows) ids.add(row.id);
|
|
44389
|
+
}
|
|
44390
|
+
return [...ids];
|
|
44391
|
+
}
|
|
44205
44392
|
async existingTrackIds(trackIds) {
|
|
44206
44393
|
const PAGE = 500;
|
|
44207
44394
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -61257,13 +61444,23 @@ var SUMMARY_MEMBERS_COLUMNS = [
|
|
|
61257
61444
|
type: "TEXT"
|
|
61258
61445
|
})
|
|
61259
61446
|
];
|
|
61260
|
-
var SUMMARY_MEMBERS_INDEXES = [
|
|
61261
|
-
|
|
61262
|
-
|
|
61263
|
-
|
|
61264
|
-
|
|
61265
|
-
|
|
61266
|
-
|
|
61447
|
+
var SUMMARY_MEMBERS_INDEXES = [
|
|
61448
|
+
{
|
|
61449
|
+
name: "summary_members_summary",
|
|
61450
|
+
columns: ["summaryId"]
|
|
61451
|
+
},
|
|
61452
|
+
{
|
|
61453
|
+
name: "summary_members_track",
|
|
61454
|
+
columns: ["trackId"]
|
|
61455
|
+
},
|
|
61456
|
+
(
|
|
61457
|
+
/** The filter's own index: "which groups hold an `animal`" walks members by
|
|
61458
|
+
* class, never summaries by hope. */
|
|
61459
|
+
{
|
|
61460
|
+
name: "summary_members_class",
|
|
61461
|
+
columns: ["className"]
|
|
61462
|
+
})
|
|
61463
|
+
];
|
|
61267
61464
|
/**
|
|
61268
61465
|
* A stored role, or `null`. An unrecognised value reads as `null` rather than
|
|
61269
61466
|
* being passed through: a consumer switching on this must not receive a string
|
|
@@ -61286,6 +61483,22 @@ function readClassName(raw) {
|
|
|
61286
61483
|
* case on a gate camera, not a race. With the track in the key a retry still
|
|
61287
61484
|
* collapses (same birth, same track) and two births stay two groups.
|
|
61288
61485
|
*/
|
|
61486
|
+
/** The widest a JS Date can name — the open end of a half-bounded window. */
|
|
61487
|
+
var TIME_MIN = -864e13;
|
|
61488
|
+
var TIME_MAX = 864e13;
|
|
61489
|
+
/**
|
|
61490
|
+
* How many rows one paged read may materialise before the merge.
|
|
61491
|
+
*
|
|
61492
|
+
* The page is cut AFTER ordering, so the read must be able to serve any cursor
|
|
61493
|
+
* position — but it must also be bounded, because an unbounded read of this
|
|
61494
|
+
* table on a busy hub is the shape D447 measured holding the settings thread
|
|
61495
|
+
* for seconds. Summaries are far fewer than tracks by construction (14 940
|
|
61496
|
+
* against 30 583 on this hub), so this ceiling is generous.
|
|
61497
|
+
*/
|
|
61498
|
+
var RECENT_SUMMARY_SCAN_LIMIT = 5e3;
|
|
61499
|
+
/** How many member rows one filter pass may read per tier. Bounded for the same
|
|
61500
|
+
* reason as the scan above, and generous: members are one row per subject. */
|
|
61501
|
+
var MEMBER_MATCH_SCAN_LIMIT = 2e4;
|
|
61289
61502
|
function summaryId(deviceId, openedAt, foundingTrackId) {
|
|
61290
61503
|
return `sum-${deviceId}-${openedAt}-${foundingTrackId}`;
|
|
61291
61504
|
}
|
|
@@ -61312,10 +61525,12 @@ var SummaryStore = class {
|
|
|
61312
61525
|
store;
|
|
61313
61526
|
logger;
|
|
61314
61527
|
onEvict;
|
|
61528
|
+
trackIdsMatching;
|
|
61315
61529
|
constructor(deps) {
|
|
61316
61530
|
this.store = deps.store;
|
|
61317
61531
|
this.logger = deps.logger;
|
|
61318
61532
|
this.onEvict = deps.onEvict;
|
|
61533
|
+
this.trackIdsMatching = deps.trackIdsMatching;
|
|
61319
61534
|
}
|
|
61320
61535
|
static async declare(store) {
|
|
61321
61536
|
await Promise.all([store.declareCollection.mutate({
|
|
@@ -61683,6 +61898,8 @@ var SummaryStore = class {
|
|
|
61683
61898
|
*/
|
|
61684
61899
|
async listRecent(input) {
|
|
61685
61900
|
if (input.deviceIds.length === 0) return [];
|
|
61901
|
+
const matching = await this.summaryIdsMatching(input.classes);
|
|
61902
|
+
if (matching !== null && matching.size === 0) return [];
|
|
61686
61903
|
const limit = Math.max(1, input.limit);
|
|
61687
61904
|
return (await this.store.query.query({
|
|
61688
61905
|
collection: SUMMARIES_COLLECTION,
|
|
@@ -61695,9 +61912,138 @@ var SummaryStore = class {
|
|
|
61695
61912
|
},
|
|
61696
61913
|
limit
|
|
61697
61914
|
}
|
|
61698
|
-
})).map((r) => toRow$1(r.id, r.data)).sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
|
|
61915
|
+
})).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
|
|
61699
61916
|
}
|
|
61700
61917
|
/** One summary with its members, or `null`. */
|
|
61918
|
+
/**
|
|
61919
|
+
* One PAGE of summaries across a camera set, newest activity first.
|
|
61920
|
+
*
|
|
61921
|
+
* The same contract as `listRecentTracks` and for the same consumers: the
|
|
61922
|
+
* reel on the cameras page and the Events gallery both ride a cursor, so a
|
|
61923
|
+
* feed that could only answer "the last N" would make groups available in the
|
|
61924
|
+
* timeline and nowhere else. `recent-cursor.ts` is reused verbatim — the
|
|
61925
|
+
* ordering is the stable total order (`lastActiveAt` DESC, id DESC), and a
|
|
61926
|
+
* second implementation of that is a second place for two feeds to disagree
|
|
61927
|
+
* about what "newest" means.
|
|
61928
|
+
*
|
|
61929
|
+
* The window is on LAST ACTIVITY, not on the open: a session that started
|
|
61930
|
+
* before the window and ran into it belongs to the window, because it
|
|
61931
|
+
* happened there.
|
|
61932
|
+
*
|
|
61933
|
+
* Members for the WHOLE page come back in one read. One query per row is how
|
|
61934
|
+
* a page of two hundred becomes two hundred round trips — the shape that made
|
|
61935
|
+
* the old digest read two thousand tracks a camera.
|
|
61936
|
+
*/
|
|
61937
|
+
async queryRecent(input) {
|
|
61938
|
+
const deviceIds = [...new Set(input.deviceIds)];
|
|
61939
|
+
if (deviceIds.length === 0) return {
|
|
61940
|
+
summaries: [],
|
|
61941
|
+
nextCursor: null
|
|
61942
|
+
};
|
|
61943
|
+
const cursor = input.cursor === void 0 ? null : decodeRecentCursor(input.cursor);
|
|
61944
|
+
const matching = await this.summaryIdsMatching(input.classes);
|
|
61945
|
+
if (matching !== null && matching.size === 0) return {
|
|
61946
|
+
summaries: [],
|
|
61947
|
+
nextCursor: null
|
|
61948
|
+
};
|
|
61949
|
+
const ordered = (await this.store.query.query({
|
|
61950
|
+
collection: SUMMARIES_COLLECTION,
|
|
61951
|
+
filter: {
|
|
61952
|
+
whereIn: { deviceId: deviceIds },
|
|
61953
|
+
...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { lastActiveAt: [input.since ?? TIME_MIN, input.until ?? TIME_MAX] } } : {},
|
|
61954
|
+
limit: RECENT_SUMMARY_SCAN_LIMIT
|
|
61955
|
+
}
|
|
61956
|
+
})).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).map((row) => ({
|
|
61957
|
+
row,
|
|
61958
|
+
key: {
|
|
61959
|
+
lastSeen: row.lastActiveAt,
|
|
61960
|
+
id: row.id
|
|
61961
|
+
}
|
|
61962
|
+
})).filter((entry) => cursor === null || isAfterCursor(entry.key, cursor)).toSorted((a, b) => compareRecentDesc(a.key, b.key));
|
|
61963
|
+
const page = ordered.slice(0, input.limit);
|
|
61964
|
+
const last = page.at(-1);
|
|
61965
|
+
const nextCursor = last !== void 0 && ordered.length > page.length ? encodeRecentCursor({
|
|
61966
|
+
lastSeen: last.row.lastActiveAt,
|
|
61967
|
+
trackId: last.row.id
|
|
61968
|
+
}) : null;
|
|
61969
|
+
const members = await this.membersOfMany(page.map((e) => e.row.id));
|
|
61970
|
+
return {
|
|
61971
|
+
summaries: page.map((e) => ({
|
|
61972
|
+
summary: e.row,
|
|
61973
|
+
members: members.get(e.row.id) ?? []
|
|
61974
|
+
})),
|
|
61975
|
+
nextCursor
|
|
61976
|
+
};
|
|
61977
|
+
}
|
|
61978
|
+
/**
|
|
61979
|
+
* The summaries whose MEMBERS match any of `classes`, or `null` for no filter.
|
|
61980
|
+
*
|
|
61981
|
+
* TRACKS first, then the members that name them, then the summaries. The
|
|
61982
|
+
* label is a fact about the track and it is asked of the track — a summary
|
|
61983
|
+
* that stored its own copy would answer with whatever was true when it
|
|
61984
|
+
* sealed, and would never learn that a face was enrolled and a person named
|
|
61985
|
+
* an hour later. The operator's rule: *a summary keeps the minimum
|
|
61986
|
+
* information necessary*, and a duplicated label is not the minimum, it is a
|
|
61987
|
+
* second truth with an expiry date.
|
|
61988
|
+
*
|
|
61989
|
+
* Narrows before the page is cut. Filtering the other way round — a page of
|
|
61990
|
+
* summaries, then their members — returns a short or empty page while matches
|
|
61991
|
+
* sit further down the list, and the operator reads that as "there were none".
|
|
61992
|
+
*/
|
|
61993
|
+
async summaryIdsMatching(classes) {
|
|
61994
|
+
const wanted = (classes ?? []).filter((t) => t.length > 0);
|
|
61995
|
+
if (wanted.length === 0) return null;
|
|
61996
|
+
const matchTracks = this.trackIdsMatching;
|
|
61997
|
+
if (matchTracks === void 0) {
|
|
61998
|
+
this.logger.warn("summary filter: no track matcher wired — refusing to answer unfiltered", { meta: { classes: wanted.length } });
|
|
61999
|
+
return /* @__PURE__ */ new Set();
|
|
62000
|
+
}
|
|
62001
|
+
const trackIds = await matchTracks(wanted);
|
|
62002
|
+
if (trackIds.length === 0) return /* @__PURE__ */ new Set();
|
|
62003
|
+
const ids = /* @__PURE__ */ new Set();
|
|
62004
|
+
const PAGE = 500;
|
|
62005
|
+
for (let i = 0; i < trackIds.length; i += PAGE) {
|
|
62006
|
+
const chunk = trackIds.slice(i, i + PAGE);
|
|
62007
|
+
const rows = await this.store.query.query({
|
|
62008
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
62009
|
+
filter: {
|
|
62010
|
+
whereIn: { trackId: [...chunk] },
|
|
62011
|
+
limit: MEMBER_MATCH_SCAN_LIMIT
|
|
62012
|
+
}
|
|
62013
|
+
});
|
|
62014
|
+
for (const row of rows) ids.add(String(row.data["summaryId"] ?? ""));
|
|
62015
|
+
}
|
|
62016
|
+
ids.delete("");
|
|
62017
|
+
return ids;
|
|
62018
|
+
}
|
|
62019
|
+
/** Members of many summaries, in ONE read, grouped by summary id. */
|
|
62020
|
+
async membersOfMany(summaryIds) {
|
|
62021
|
+
const out = /* @__PURE__ */ new Map();
|
|
62022
|
+
if (summaryIds.length === 0) return out;
|
|
62023
|
+
const rows = await this.store.query.query({
|
|
62024
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
62025
|
+
filter: {
|
|
62026
|
+
whereIn: { summaryId: [...summaryIds] },
|
|
62027
|
+
limit: 1e4
|
|
62028
|
+
}
|
|
62029
|
+
});
|
|
62030
|
+
for (const r of rows) {
|
|
62031
|
+
const summaryIdValue = String(r.data["summaryId"] ?? "");
|
|
62032
|
+
const member = {
|
|
62033
|
+
summaryId: summaryIdValue,
|
|
62034
|
+
trackId: String(r.data["trackId"] ?? ""),
|
|
62035
|
+
deviceId: num$1(r.data["deviceId"]),
|
|
62036
|
+
joinedAt: num$1(r.data["timestamp"]),
|
|
62037
|
+
role: readRole(r.data["role"]),
|
|
62038
|
+
className: readClassName(r.data["className"])
|
|
62039
|
+
};
|
|
62040
|
+
const bucket = out.get(summaryIdValue);
|
|
62041
|
+
if (bucket === void 0) out.set(summaryIdValue, [member]);
|
|
62042
|
+
else bucket.push(member);
|
|
62043
|
+
}
|
|
62044
|
+
for (const bucket of out.values()) bucket.sort((a, b) => a.joinedAt - b.joinedAt);
|
|
62045
|
+
return out;
|
|
62046
|
+
}
|
|
61701
62047
|
async get(id) {
|
|
61702
62048
|
const raw = await this.store.get.query({
|
|
61703
62049
|
collection: SUMMARIES_COLLECTION,
|
|
@@ -71988,6 +72334,33 @@ async function persistSummaryGroupShot(deps, input) {
|
|
|
71988
72334
|
}
|
|
71989
72335
|
}
|
|
71990
72336
|
//#endregion
|
|
72337
|
+
//#region src/pipeline-analytics/summary/summary-composition.ts
|
|
72338
|
+
function summaryComposition(members) {
|
|
72339
|
+
const counts = /* @__PURE__ */ new Map();
|
|
72340
|
+
for (const member of members) counts.set(member.className, (counts.get(member.className) ?? 0) + 1);
|
|
72341
|
+
return { classes: [...counts].map(([className, count]) => ({
|
|
72342
|
+
className,
|
|
72343
|
+
count
|
|
72344
|
+
})).toSorted((a, b) => a.count !== b.count ? b.count - a.count : (a.className ?? "").localeCompare(b.className ?? "")) };
|
|
72345
|
+
}
|
|
72346
|
+
//#endregion
|
|
72347
|
+
//#region src/pipeline-analytics/summary/summary-member-labels.ts
|
|
72348
|
+
function present(value) {
|
|
72349
|
+
return typeof value === "string" && value.length > 0;
|
|
72350
|
+
}
|
|
72351
|
+
function withMemberLabels(members, tracks) {
|
|
72352
|
+
const byId = new Map(tracks.map((t) => [t.trackId, t]));
|
|
72353
|
+
return members.map((member) => {
|
|
72354
|
+
const track = byId.get(member.trackId);
|
|
72355
|
+
return {
|
|
72356
|
+
...member,
|
|
72357
|
+
className: member.className ?? (present(track?.className) ? track.className : null),
|
|
72358
|
+
...present(track?.label) ? { label: track.label } : {},
|
|
72359
|
+
...present(track?.subLabel) ? { subLabel: track.subLabel } : {}
|
|
72360
|
+
};
|
|
72361
|
+
});
|
|
72362
|
+
}
|
|
72363
|
+
//#endregion
|
|
71991
72364
|
//#region src/pipeline-analytics/summary/summary-join.ts
|
|
71992
72365
|
/**
|
|
71993
72366
|
* Which open group a newborn track joins — or none, and it opens its own.
|
|
@@ -72333,7 +72706,21 @@ var SummarySealer = class {
|
|
|
72333
72706
|
return null;
|
|
72334
72707
|
}
|
|
72335
72708
|
}
|
|
72336
|
-
|
|
72709
|
+
/**
|
|
72710
|
+
* Is this group finished?
|
|
72711
|
+
*
|
|
72712
|
+
* Both questions are about THIS group's members, not about the camera. A
|
|
72713
|
+
* camera holds as many groups as it has (D489), so a camera-wide gate means
|
|
72714
|
+
* no group seals until every subject on the camera has gone — and on a busy
|
|
72715
|
+
* camera they never all go at once. Measured: eight summaries open on one
|
|
72716
|
+
* camera over seventeen minutes, none sealed, each new subject opening a
|
|
72717
|
+
* ninth because a group whose members have closed has no positions left for
|
|
72718
|
+
* the proximity rule to judge.
|
|
72719
|
+
*
|
|
72720
|
+
* `null` means "could not find out", and the caller treats that as NOT done —
|
|
72721
|
+
* a read that failed must never seal a live session (D49).
|
|
72722
|
+
*/
|
|
72723
|
+
async isGroupFinished(summary, members) {
|
|
72337
72724
|
const { id: summaryId, deviceId } = summary;
|
|
72338
72725
|
let openTracks;
|
|
72339
72726
|
try {
|
|
@@ -72346,12 +72733,13 @@ var SummarySealer = class {
|
|
|
72346
72733
|
error: errMsg(err)
|
|
72347
72734
|
}
|
|
72348
72735
|
});
|
|
72349
|
-
return;
|
|
72736
|
+
return null;
|
|
72350
72737
|
}
|
|
72351
|
-
|
|
72352
|
-
|
|
72738
|
+
const stillOpen = new Set(openTracks);
|
|
72739
|
+
if (members.some((m) => stillOpen.has(m))) return false;
|
|
72740
|
+
let parked;
|
|
72353
72741
|
try {
|
|
72354
|
-
|
|
72742
|
+
parked = await this.deps.parkedTrackIds(deviceId);
|
|
72355
72743
|
} catch (err) {
|
|
72356
72744
|
this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
|
|
72357
72745
|
tags: { deviceId },
|
|
@@ -72360,9 +72748,26 @@ var SummarySealer = class {
|
|
|
72360
72748
|
error: errMsg(err)
|
|
72361
72749
|
}
|
|
72362
72750
|
});
|
|
72751
|
+
return null;
|
|
72752
|
+
}
|
|
72753
|
+
return !members.some((m) => parked.has(m));
|
|
72754
|
+
}
|
|
72755
|
+
async sealIfDone(summary) {
|
|
72756
|
+
const { id: summaryId, deviceId } = summary;
|
|
72757
|
+
let members;
|
|
72758
|
+
try {
|
|
72759
|
+
members = (await this.deps.summaries.membersOf(summaryId)).map((m) => m.trackId);
|
|
72760
|
+
} catch (err) {
|
|
72761
|
+
this.deps.logger.warn("summary sweep: could not read members — seal aborted this sweep", {
|
|
72762
|
+
tags: { deviceId },
|
|
72763
|
+
meta: {
|
|
72764
|
+
summaryId,
|
|
72765
|
+
error: errMsg(err)
|
|
72766
|
+
}
|
|
72767
|
+
});
|
|
72363
72768
|
return;
|
|
72364
72769
|
}
|
|
72365
|
-
if (
|
|
72770
|
+
if (await this.isGroupFinished(summary, members) !== true) return;
|
|
72366
72771
|
const sealedAt = this.deps.now();
|
|
72367
72772
|
try {
|
|
72368
72773
|
if (await this.deps.summaries.seal(summary, sealedAt)) {
|
|
@@ -73968,6 +74373,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73968
74373
|
await secretState.set(secret);
|
|
73969
74374
|
}
|
|
73970
74375
|
const shelfWindows = await this.pruneWindows();
|
|
74376
|
+
const index = new NcArtifactIndex({
|
|
74377
|
+
store: this.ctx.api.settingsStore,
|
|
74378
|
+
logger: this.ctx.logger.child("nc-artifacts"),
|
|
74379
|
+
addonId: this.ctx.id,
|
|
74380
|
+
retentionMs: shelfWindows.artifactShelfMs
|
|
74381
|
+
});
|
|
74382
|
+
this.ncArtifactIndex = index;
|
|
74383
|
+
const refusal = artifactShelfRefusal(await this.ctx.api.storage.listLocations.query({}), this.ctx.kernel.localNodeId ?? null);
|
|
74384
|
+
if (refusal !== null) {
|
|
74385
|
+
this.ctx.logger.info("nc-artifact shelf not served on this node", { meta: {
|
|
74386
|
+
reason: refusal,
|
|
74387
|
+
indexServed: true
|
|
74388
|
+
} });
|
|
74389
|
+
return;
|
|
74390
|
+
}
|
|
73971
74391
|
const store = new NcArtifactStore({
|
|
73972
74392
|
rootDir: await this.resolveNcArtifactRoot(),
|
|
73973
74393
|
logger: this.ctx.logger.child("nc-artifacts"),
|
|
@@ -73975,13 +74395,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73975
74395
|
});
|
|
73976
74396
|
await store.start();
|
|
73977
74397
|
this.ncArtifactStore = store;
|
|
73978
|
-
const index = new NcArtifactIndex({
|
|
73979
|
-
store: this.ctx.api.settingsStore,
|
|
73980
|
-
logger: this.ctx.logger.child("nc-artifacts"),
|
|
73981
|
-
addonId: this.ctx.id,
|
|
73982
|
-
retentionMs: store.retentionMs
|
|
73983
|
-
});
|
|
73984
|
-
this.ncArtifactIndex = index;
|
|
73985
74398
|
const plane = new NcArtifactPlane({
|
|
73986
74399
|
store,
|
|
73987
74400
|
secret,
|
|
@@ -75331,6 +75744,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75331
75744
|
logger: logger.child("SensorEventStore")
|
|
75332
75745
|
});
|
|
75333
75746
|
this.summaryStore = new SummaryStore({
|
|
75747
|
+
trackIdsMatching: async (terms) => await this.trackStore?.trackIdsMatching(terms) ?? [],
|
|
75334
75748
|
store: api.settingsStore,
|
|
75335
75749
|
logger: this.ctx.logger.child("summary"),
|
|
75336
75750
|
onEvict: async (ids) => {
|
|
@@ -75341,7 +75755,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75341
75755
|
summaries: this.summaryStore,
|
|
75342
75756
|
analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
|
|
75343
75757
|
openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
|
|
75344
|
-
|
|
75758
|
+
parkedTrackIds: async (deviceId) => {
|
|
75759
|
+
const ids = /* @__PURE__ */ new Set();
|
|
75760
|
+
for (const entry of this.stationaryRegistry?.list(deviceId) ?? []) if (entry.sourceTrackId !== void 0) ids.add(entry.sourceTrackId);
|
|
75761
|
+
return ids;
|
|
75762
|
+
},
|
|
75345
75763
|
now: () => Date.now(),
|
|
75346
75764
|
logger: this.ctx.logger.child("summary-sealer"),
|
|
75347
75765
|
frameSize: (deviceId) => {
|
|
@@ -76046,6 +76464,30 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76046
76464
|
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
76047
76465
|
}));
|
|
76048
76466
|
},
|
|
76467
|
+
listSummaryGroups: async ({ deviceId, sinceMs, untilMs }) => {
|
|
76468
|
+
const summaryStore = this.summaryStore;
|
|
76469
|
+
if (!summaryStore) return [];
|
|
76470
|
+
const rows = await summaryStore.listRecent({
|
|
76471
|
+
deviceIds: [deviceId],
|
|
76472
|
+
since: sinceMs,
|
|
76473
|
+
until: untilMs,
|
|
76474
|
+
limit: 500
|
|
76475
|
+
});
|
|
76476
|
+
const out = [];
|
|
76477
|
+
for (const row of rows) {
|
|
76478
|
+
const members = await summaryStore.membersOf(row.id);
|
|
76479
|
+
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|
|
76480
|
+
const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
|
|
76481
|
+
out.push({
|
|
76482
|
+
summaryId: row.id,
|
|
76483
|
+
deviceId: row.deviceId,
|
|
76484
|
+
memberTrackIds: members.map((m) => m.trackId),
|
|
76485
|
+
primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null,
|
|
76486
|
+
groupShotMediaId: shot?.mediaId ?? null
|
|
76487
|
+
});
|
|
76488
|
+
}
|
|
76489
|
+
return out;
|
|
76490
|
+
},
|
|
76049
76491
|
listMediaInfo: async (id) => {
|
|
76050
76492
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
76051
76493
|
mediaId: r.mediaId,
|
|
@@ -81194,23 +81636,96 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
81194
81636
|
deviceIds: input.deviceIds,
|
|
81195
81637
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
81196
81638
|
...input.until !== void 0 ? { until: input.until } : {},
|
|
81197
|
-
limit: input.limit ?? 50
|
|
81639
|
+
limit: input.limit ?? 50,
|
|
81640
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81198
81641
|
});
|
|
81199
|
-
return { summaries: await
|
|
81642
|
+
return { summaries: await this.listItemsFor(rows) };
|
|
81200
81643
|
}
|
|
81201
|
-
|
|
81202
|
-
|
|
81203
|
-
|
|
81644
|
+
/**
|
|
81645
|
+
* Rows → what a LIST draws: the envelope plus the composition.
|
|
81646
|
+
*
|
|
81647
|
+
* Two bulk reads for the whole page, never per row: the members in one, their
|
|
81648
|
+
* tracks (for the names) in one. The MEMBERS themselves stay off the list —
|
|
81649
|
+
* a page of two hundred groups would otherwise carry a thousand records so
|
|
81650
|
+
* the client could count them. `getSummary` is where the links live.
|
|
81651
|
+
*/
|
|
81652
|
+
async listItemsFor(rows) {
|
|
81653
|
+
const store = this.summaryStore;
|
|
81654
|
+
if (!store || rows.length === 0) return [];
|
|
81655
|
+
const members = await store.membersOfMany(rows.map((r) => r.id));
|
|
81656
|
+
return await Promise.all(rows.map(async (row) => ({
|
|
81657
|
+
summary: await this.toSummary(row),
|
|
81658
|
+
...summaryComposition(members.get(row.id) ?? [])
|
|
81659
|
+
})));
|
|
81660
|
+
}
|
|
81661
|
+
/** The members of a page of summaries, with the labels a card can show —
|
|
81662
|
+
* in ONE read of the members and ONE of their tracks. */
|
|
81663
|
+
async labelledMembersFor(rows) {
|
|
81664
|
+
const out = /* @__PURE__ */ new Map();
|
|
81665
|
+
const store = this.summaryStore;
|
|
81666
|
+
if (!store || rows.length === 0) return out;
|
|
81667
|
+
const membersBySummary = await store.membersOfMany(rows.map((r) => r.id));
|
|
81668
|
+
const trackIds = [...new Set([...membersBySummary.values()].flatMap((ms) => ms.map((m) => m.trackId)))];
|
|
81669
|
+
let tracks = [];
|
|
81670
|
+
try {
|
|
81671
|
+
tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
|
|
81672
|
+
} catch (err) {
|
|
81673
|
+
this.ctx.logger.warn("summary members: labels unavailable for this page", { meta: {
|
|
81674
|
+
summaries: rows.length,
|
|
81675
|
+
members: trackIds.length,
|
|
81676
|
+
error: errMsg$1(err)
|
|
81677
|
+
} });
|
|
81678
|
+
}
|
|
81679
|
+
for (const row of rows) out.set(row.id, withMemberLabels(membersBySummary.get(row.id) ?? [], tracks));
|
|
81680
|
+
return out;
|
|
81681
|
+
}
|
|
81682
|
+
/**
|
|
81683
|
+
* Rows → the shape every summary surface answers with: the envelope, its
|
|
81684
|
+
* members, and what a thumbnail can render for each of them.
|
|
81685
|
+
*
|
|
81686
|
+
* THREE reads for the whole page, never per row: the members in one, their
|
|
81687
|
+
* tracks in one, and the group shots through `toSummary`. A read per member
|
|
81688
|
+
* is what made a sixty-group page take twenty seconds on the events feed.
|
|
81689
|
+
*/
|
|
81690
|
+
/** ONE summary, with every member and the link each of them carries. */
|
|
81691
|
+
async detailFor(row) {
|
|
81692
|
+
const labelled = (await this.labelledMembersFor([row])).get(row.id) ?? [];
|
|
81204
81693
|
return {
|
|
81205
|
-
summary: await this.toSummary(
|
|
81206
|
-
members:
|
|
81694
|
+
summary: await this.toSummary(row),
|
|
81695
|
+
members: labelled.map((m) => ({
|
|
81207
81696
|
trackId: m.trackId,
|
|
81208
81697
|
className: m.className,
|
|
81209
81698
|
role: m.role,
|
|
81210
|
-
joinedAt: m.joinedAt
|
|
81699
|
+
joinedAt: m.joinedAt,
|
|
81700
|
+
...m.label !== void 0 ? { label: m.label } : {},
|
|
81701
|
+
...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
|
|
81211
81702
|
}))
|
|
81212
81703
|
};
|
|
81213
81704
|
}
|
|
81705
|
+
async listRecentSummaries(input) {
|
|
81706
|
+
const store = this.summaryStore;
|
|
81707
|
+
if (!store) return {
|
|
81708
|
+
summaries: [],
|
|
81709
|
+
nextCursor: null
|
|
81710
|
+
};
|
|
81711
|
+
const page = await store.queryRecent({
|
|
81712
|
+
deviceIds: input.deviceIds,
|
|
81713
|
+
...input.since !== void 0 ? { since: input.since } : {},
|
|
81714
|
+
...input.until !== void 0 ? { until: input.until } : {},
|
|
81715
|
+
limit: input.limit,
|
|
81716
|
+
...input.cursor !== void 0 ? { cursor: input.cursor } : {},
|
|
81717
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81718
|
+
});
|
|
81719
|
+
return {
|
|
81720
|
+
summaries: await this.listItemsFor(page.summaries.map((d) => d.summary)),
|
|
81721
|
+
nextCursor: page.nextCursor
|
|
81722
|
+
};
|
|
81723
|
+
}
|
|
81724
|
+
async getSummary(input) {
|
|
81725
|
+
const detail = await this.summaryStore?.get(input.id);
|
|
81726
|
+
if (!detail) return null;
|
|
81727
|
+
return await this.detailFor(detail.summary);
|
|
81728
|
+
}
|
|
81214
81729
|
/** The row plus its group shot: the web companion when it landed, else the native key frame. */
|
|
81215
81730
|
async toSummary(row) {
|
|
81216
81731
|
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|