@camstack/addon-post-analysis 1.2.231 → 1.2.232
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-w7xIpvHc.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-2-KE2jBo.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +605 -42
- package/dist/pipeline-analytics/index.mjs +605 -42
- 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(),
|
|
@@ -61255,15 +61402,48 @@ var SUMMARY_MEMBERS_COLUMNS = [
|
|
|
61255
61402
|
{
|
|
61256
61403
|
name: "className",
|
|
61257
61404
|
type: "TEXT"
|
|
61405
|
+
}),
|
|
61406
|
+
(
|
|
61407
|
+
/**
|
|
61408
|
+
* The member's tier-1 label (`dog`, `van`) and tier-2 instance (a name, a
|
|
61409
|
+
* plate), STAMPED AT SEAL.
|
|
61410
|
+
*
|
|
61411
|
+
* Not at the join, and the timing is the whole point: recognition lands
|
|
61412
|
+
* during a track's life, almost always after it joined, so a label written at
|
|
61413
|
+
* the join would be empty on exactly the members worth naming. At the seal
|
|
61414
|
+
* every member's track has closed, so its label is final — there is nothing
|
|
61415
|
+
* left to change it.
|
|
61416
|
+
*
|
|
61417
|
+
* They are COLUMNS because a filter has to be able to ask for them: "show me
|
|
61418
|
+
* the arrivals with a dog" is a query over members, and answering it from the
|
|
61419
|
+
* tracks would mean reading every track of every summary in the window.
|
|
61420
|
+
*/
|
|
61421
|
+
{
|
|
61422
|
+
name: "label",
|
|
61423
|
+
type: "TEXT"
|
|
61424
|
+
}),
|
|
61425
|
+
{
|
|
61426
|
+
name: "subLabel",
|
|
61427
|
+
type: "TEXT"
|
|
61428
|
+
}
|
|
61429
|
+
];
|
|
61430
|
+
var SUMMARY_MEMBERS_INDEXES = [
|
|
61431
|
+
{
|
|
61432
|
+
name: "summary_members_summary",
|
|
61433
|
+
columns: ["summaryId"]
|
|
61434
|
+
},
|
|
61435
|
+
{
|
|
61436
|
+
name: "summary_members_track",
|
|
61437
|
+
columns: ["trackId"]
|
|
61438
|
+
},
|
|
61439
|
+
(
|
|
61440
|
+
/** The filter's own index: "which groups hold an `animal`" walks members by
|
|
61441
|
+
* class, never summaries by hope. */
|
|
61442
|
+
{
|
|
61443
|
+
name: "summary_members_class",
|
|
61444
|
+
columns: ["className"]
|
|
61258
61445
|
})
|
|
61259
61446
|
];
|
|
61260
|
-
var SUMMARY_MEMBERS_INDEXES = [{
|
|
61261
|
-
name: "summary_members_summary",
|
|
61262
|
-
columns: ["summaryId"]
|
|
61263
|
-
}, {
|
|
61264
|
-
name: "summary_members_track",
|
|
61265
|
-
columns: ["trackId"]
|
|
61266
|
-
}];
|
|
61267
61447
|
/**
|
|
61268
61448
|
* A stored role, or `null`. An unrecognised value reads as `null` rather than
|
|
61269
61449
|
* being passed through: a consumer switching on this must not receive a string
|
|
@@ -61273,6 +61453,16 @@ var SUMMARY_MEMBERS_INDEXES = [{
|
|
|
61273
61453
|
function readRole(raw) {
|
|
61274
61454
|
return raw === "primary" || raw === "sibling" || raw === "child" ? raw : null;
|
|
61275
61455
|
}
|
|
61456
|
+
/** The two stamped labels, present only when they hold something. An empty
|
|
61457
|
+
* string is not a label and must never reach a filter as one. */
|
|
61458
|
+
function labelFields(data) {
|
|
61459
|
+
const label = data["label"];
|
|
61460
|
+
const subLabel = data["subLabel"];
|
|
61461
|
+
return {
|
|
61462
|
+
...typeof label === "string" && label.length > 0 ? { label } : {},
|
|
61463
|
+
...typeof subLabel === "string" && subLabel.length > 0 ? { subLabel } : {}
|
|
61464
|
+
};
|
|
61465
|
+
}
|
|
61276
61466
|
function readClassName(raw) {
|
|
61277
61467
|
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
61278
61468
|
}
|
|
@@ -61286,6 +61476,22 @@ function readClassName(raw) {
|
|
|
61286
61476
|
* case on a gate camera, not a race. With the track in the key a retry still
|
|
61287
61477
|
* collapses (same birth, same track) and two births stay two groups.
|
|
61288
61478
|
*/
|
|
61479
|
+
/** The widest a JS Date can name — the open end of a half-bounded window. */
|
|
61480
|
+
var TIME_MIN = -864e13;
|
|
61481
|
+
var TIME_MAX = 864e13;
|
|
61482
|
+
/**
|
|
61483
|
+
* How many rows one paged read may materialise before the merge.
|
|
61484
|
+
*
|
|
61485
|
+
* The page is cut AFTER ordering, so the read must be able to serve any cursor
|
|
61486
|
+
* position — but it must also be bounded, because an unbounded read of this
|
|
61487
|
+
* table on a busy hub is the shape D447 measured holding the settings thread
|
|
61488
|
+
* for seconds. Summaries are far fewer than tracks by construction (14 940
|
|
61489
|
+
* against 30 583 on this hub), so this ceiling is generous.
|
|
61490
|
+
*/
|
|
61491
|
+
var RECENT_SUMMARY_SCAN_LIMIT = 5e3;
|
|
61492
|
+
/** How many member rows one filter pass may read per tier. Bounded for the same
|
|
61493
|
+
* reason as the scan above, and generous: members are one row per subject. */
|
|
61494
|
+
var MEMBER_MATCH_SCAN_LIMIT = 2e4;
|
|
61289
61495
|
function summaryId(deviceId, openedAt, foundingTrackId) {
|
|
61290
61496
|
return `sum-${deviceId}-${openedAt}-${foundingTrackId}`;
|
|
61291
61497
|
}
|
|
@@ -61370,7 +61576,8 @@ var SummaryStore = class {
|
|
|
61370
61576
|
deviceId: num$1(r.data["deviceId"]),
|
|
61371
61577
|
joinedAt: num$1(r.data["timestamp"]),
|
|
61372
61578
|
role: readRole(r.data["role"]),
|
|
61373
|
-
className: readClassName(r.data["className"])
|
|
61579
|
+
className: readClassName(r.data["className"]),
|
|
61580
|
+
...labelFields(r.data)
|
|
61374
61581
|
}));
|
|
61375
61582
|
}
|
|
61376
61583
|
/**
|
|
@@ -61558,6 +61765,23 @@ var SummaryStore = class {
|
|
|
61558
61765
|
memberCount: leaving.length
|
|
61559
61766
|
};
|
|
61560
61767
|
}
|
|
61768
|
+
/** Stamp a member's final labels. Best-effort and loud: a missing stamp
|
|
61769
|
+
* costs a filter a match, never a membership. */
|
|
61770
|
+
async stampMemberLabels(summaryIdValue, trackId, labels) {
|
|
61771
|
+
try {
|
|
61772
|
+
await this.store.update.mutate({
|
|
61773
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
61774
|
+
id: memberRowId(summaryIdValue, trackId),
|
|
61775
|
+
data: { ...labels }
|
|
61776
|
+
});
|
|
61777
|
+
} catch (err) {
|
|
61778
|
+
this.logger.warn("summary: could not stamp a member label", { meta: {
|
|
61779
|
+
summaryId: summaryIdValue,
|
|
61780
|
+
trackId,
|
|
61781
|
+
error: String(err)
|
|
61782
|
+
} });
|
|
61783
|
+
}
|
|
61784
|
+
}
|
|
61561
61785
|
/**
|
|
61562
61786
|
* Put every OTHER member back to `sibling` after a new primary took the role.
|
|
61563
61787
|
*
|
|
@@ -61683,6 +61907,8 @@ var SummaryStore = class {
|
|
|
61683
61907
|
*/
|
|
61684
61908
|
async listRecent(input) {
|
|
61685
61909
|
if (input.deviceIds.length === 0) return [];
|
|
61910
|
+
const matching = await this.summaryIdsMatching(input.classes);
|
|
61911
|
+
if (matching !== null && matching.size === 0) return [];
|
|
61686
61912
|
const limit = Math.max(1, input.limit);
|
|
61687
61913
|
return (await this.store.query.query({
|
|
61688
61914
|
collection: SUMMARIES_COLLECTION,
|
|
@@ -61695,9 +61921,132 @@ var SummaryStore = class {
|
|
|
61695
61921
|
},
|
|
61696
61922
|
limit
|
|
61697
61923
|
}
|
|
61698
|
-
})).map((r) => toRow$1(r.id, r.data)).sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
|
|
61924
|
+
})).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
61925
|
}
|
|
61700
61926
|
/** One summary with its members, or `null`. */
|
|
61927
|
+
/**
|
|
61928
|
+
* One PAGE of summaries across a camera set, newest activity first.
|
|
61929
|
+
*
|
|
61930
|
+
* The same contract as `listRecentTracks` and for the same consumers: the
|
|
61931
|
+
* reel on the cameras page and the Events gallery both ride a cursor, so a
|
|
61932
|
+
* feed that could only answer "the last N" would make groups available in the
|
|
61933
|
+
* timeline and nowhere else. `recent-cursor.ts` is reused verbatim — the
|
|
61934
|
+
* ordering is the stable total order (`lastActiveAt` DESC, id DESC), and a
|
|
61935
|
+
* second implementation of that is a second place for two feeds to disagree
|
|
61936
|
+
* about what "newest" means.
|
|
61937
|
+
*
|
|
61938
|
+
* The window is on LAST ACTIVITY, not on the open: a session that started
|
|
61939
|
+
* before the window and ran into it belongs to the window, because it
|
|
61940
|
+
* happened there.
|
|
61941
|
+
*
|
|
61942
|
+
* Members for the WHOLE page come back in one read. One query per row is how
|
|
61943
|
+
* a page of two hundred becomes two hundred round trips — the shape that made
|
|
61944
|
+
* the old digest read two thousand tracks a camera.
|
|
61945
|
+
*/
|
|
61946
|
+
async queryRecent(input) {
|
|
61947
|
+
const deviceIds = [...new Set(input.deviceIds)];
|
|
61948
|
+
if (deviceIds.length === 0) return {
|
|
61949
|
+
summaries: [],
|
|
61950
|
+
nextCursor: null
|
|
61951
|
+
};
|
|
61952
|
+
const cursor = input.cursor === void 0 ? null : decodeRecentCursor(input.cursor);
|
|
61953
|
+
const matching = await this.summaryIdsMatching(input.classes);
|
|
61954
|
+
if (matching !== null && matching.size === 0) return {
|
|
61955
|
+
summaries: [],
|
|
61956
|
+
nextCursor: null
|
|
61957
|
+
};
|
|
61958
|
+
const ordered = (await this.store.query.query({
|
|
61959
|
+
collection: SUMMARIES_COLLECTION,
|
|
61960
|
+
filter: {
|
|
61961
|
+
whereIn: { deviceId: deviceIds },
|
|
61962
|
+
...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { lastActiveAt: [input.since ?? TIME_MIN, input.until ?? TIME_MAX] } } : {},
|
|
61963
|
+
limit: RECENT_SUMMARY_SCAN_LIMIT
|
|
61964
|
+
}
|
|
61965
|
+
})).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).map((row) => ({
|
|
61966
|
+
row,
|
|
61967
|
+
key: {
|
|
61968
|
+
lastSeen: row.lastActiveAt,
|
|
61969
|
+
id: row.id
|
|
61970
|
+
}
|
|
61971
|
+
})).filter((entry) => cursor === null || isAfterCursor(entry.key, cursor)).toSorted((a, b) => compareRecentDesc(a.key, b.key));
|
|
61972
|
+
const page = ordered.slice(0, input.limit);
|
|
61973
|
+
const last = page.at(-1);
|
|
61974
|
+
const nextCursor = last !== void 0 && ordered.length > page.length ? encodeRecentCursor({
|
|
61975
|
+
lastSeen: last.row.lastActiveAt,
|
|
61976
|
+
trackId: last.row.id
|
|
61977
|
+
}) : null;
|
|
61978
|
+
const members = await this.membersOfMany(page.map((e) => e.row.id));
|
|
61979
|
+
return {
|
|
61980
|
+
summaries: page.map((e) => ({
|
|
61981
|
+
summary: e.row,
|
|
61982
|
+
members: members.get(e.row.id) ?? []
|
|
61983
|
+
})),
|
|
61984
|
+
nextCursor
|
|
61985
|
+
};
|
|
61986
|
+
}
|
|
61987
|
+
/**
|
|
61988
|
+
* The summaries whose MEMBERS match any of `classes`, or `null` for no filter.
|
|
61989
|
+
*
|
|
61990
|
+
* Narrows first, on the members table, and only then reads summary rows.
|
|
61991
|
+
* Filtering the other way round — a page of summaries, then their members —
|
|
61992
|
+
* returns a short or empty page while matches sit further down the list, and
|
|
61993
|
+
* the operator reads that as "there were none".
|
|
61994
|
+
*
|
|
61995
|
+
* Three queries because the filter compiler has no OR across fields, and one
|
|
61996
|
+
* term can be any of the three tiers: `animal` is a class, `dog` is a
|
|
61997
|
+
* sub-class, `Roberta` is an instance. The caller does not have to know
|
|
61998
|
+
* which — that is the whole point of the operator's example.
|
|
61999
|
+
*/
|
|
62000
|
+
async summaryIdsMatching(classes) {
|
|
62001
|
+
const wanted = (classes ?? []).filter((t) => t.length > 0);
|
|
62002
|
+
if (wanted.length === 0) return null;
|
|
62003
|
+
const ids = /* @__PURE__ */ new Set();
|
|
62004
|
+
for (const field of [
|
|
62005
|
+
"className",
|
|
62006
|
+
"label",
|
|
62007
|
+
"subLabel"
|
|
62008
|
+
]) {
|
|
62009
|
+
const rows = await this.store.query.query({
|
|
62010
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
62011
|
+
filter: {
|
|
62012
|
+
whereIn: { [field]: [...wanted] },
|
|
62013
|
+
limit: MEMBER_MATCH_SCAN_LIMIT
|
|
62014
|
+
}
|
|
62015
|
+
});
|
|
62016
|
+
for (const row of rows) ids.add(String(row.data["summaryId"] ?? ""));
|
|
62017
|
+
}
|
|
62018
|
+
ids.delete("");
|
|
62019
|
+
return ids;
|
|
62020
|
+
}
|
|
62021
|
+
/** Members of many summaries, in ONE read, grouped by summary id. */
|
|
62022
|
+
async membersOfMany(summaryIds) {
|
|
62023
|
+
const out = /* @__PURE__ */ new Map();
|
|
62024
|
+
if (summaryIds.length === 0) return out;
|
|
62025
|
+
const rows = await this.store.query.query({
|
|
62026
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
62027
|
+
filter: {
|
|
62028
|
+
whereIn: { summaryId: [...summaryIds] },
|
|
62029
|
+
limit: 1e4
|
|
62030
|
+
}
|
|
62031
|
+
});
|
|
62032
|
+
for (const r of rows) {
|
|
62033
|
+
const summaryIdValue = String(r.data["summaryId"] ?? "");
|
|
62034
|
+
const member = {
|
|
62035
|
+
summaryId: summaryIdValue,
|
|
62036
|
+
trackId: String(r.data["trackId"] ?? ""),
|
|
62037
|
+
deviceId: num$1(r.data["deviceId"]),
|
|
62038
|
+
joinedAt: num$1(r.data["timestamp"]),
|
|
62039
|
+
role: readRole(r.data["role"]),
|
|
62040
|
+
className: readClassName(r.data["className"]),
|
|
62041
|
+
...labelFields(r.data)
|
|
62042
|
+
};
|
|
62043
|
+
const bucket = out.get(summaryIdValue);
|
|
62044
|
+
if (bucket === void 0) out.set(summaryIdValue, [member]);
|
|
62045
|
+
else bucket.push(member);
|
|
62046
|
+
}
|
|
62047
|
+
for (const bucket of out.values()) bucket.sort((a, b) => a.joinedAt - b.joinedAt);
|
|
62048
|
+
return out;
|
|
62049
|
+
}
|
|
61701
62050
|
async get(id) {
|
|
61702
62051
|
const raw = await this.store.get.query({
|
|
61703
62052
|
collection: SUMMARIES_COLLECTION,
|
|
@@ -71988,6 +72337,33 @@ async function persistSummaryGroupShot(deps, input) {
|
|
|
71988
72337
|
}
|
|
71989
72338
|
}
|
|
71990
72339
|
//#endregion
|
|
72340
|
+
//#region src/pipeline-analytics/summary/summary-composition.ts
|
|
72341
|
+
function summaryComposition(members) {
|
|
72342
|
+
const counts = /* @__PURE__ */ new Map();
|
|
72343
|
+
for (const member of members) counts.set(member.className, (counts.get(member.className) ?? 0) + 1);
|
|
72344
|
+
return { classes: [...counts].map(([className, count]) => ({
|
|
72345
|
+
className,
|
|
72346
|
+
count
|
|
72347
|
+
})).toSorted((a, b) => a.count !== b.count ? b.count - a.count : (a.className ?? "").localeCompare(b.className ?? "")) };
|
|
72348
|
+
}
|
|
72349
|
+
//#endregion
|
|
72350
|
+
//#region src/pipeline-analytics/summary/summary-member-labels.ts
|
|
72351
|
+
function present(value) {
|
|
72352
|
+
return typeof value === "string" && value.length > 0;
|
|
72353
|
+
}
|
|
72354
|
+
function withMemberLabels(members, tracks) {
|
|
72355
|
+
const byId = new Map(tracks.map((t) => [t.trackId, t]));
|
|
72356
|
+
return members.map((member) => {
|
|
72357
|
+
const track = byId.get(member.trackId);
|
|
72358
|
+
return {
|
|
72359
|
+
...member,
|
|
72360
|
+
className: member.className ?? (present(track?.className) ? track.className : null),
|
|
72361
|
+
...present(track?.label) ? { label: track.label } : {},
|
|
72362
|
+
...present(track?.subLabel) ? { subLabel: track.subLabel } : {}
|
|
72363
|
+
};
|
|
72364
|
+
});
|
|
72365
|
+
}
|
|
72366
|
+
//#endregion
|
|
71991
72367
|
//#region src/pipeline-analytics/summary/summary-join.ts
|
|
71992
72368
|
/**
|
|
71993
72369
|
* Which open group a newborn track joins — or none, and it opens its own.
|
|
@@ -72333,7 +72709,21 @@ var SummarySealer = class {
|
|
|
72333
72709
|
return null;
|
|
72334
72710
|
}
|
|
72335
72711
|
}
|
|
72336
|
-
|
|
72712
|
+
/**
|
|
72713
|
+
* Is this group finished?
|
|
72714
|
+
*
|
|
72715
|
+
* Both questions are about THIS group's members, not about the camera. A
|
|
72716
|
+
* camera holds as many groups as it has (D489), so a camera-wide gate means
|
|
72717
|
+
* no group seals until every subject on the camera has gone — and on a busy
|
|
72718
|
+
* camera they never all go at once. Measured: eight summaries open on one
|
|
72719
|
+
* camera over seventeen minutes, none sealed, each new subject opening a
|
|
72720
|
+
* ninth because a group whose members have closed has no positions left for
|
|
72721
|
+
* the proximity rule to judge.
|
|
72722
|
+
*
|
|
72723
|
+
* `null` means "could not find out", and the caller treats that as NOT done —
|
|
72724
|
+
* a read that failed must never seal a live session (D49).
|
|
72725
|
+
*/
|
|
72726
|
+
async isGroupFinished(summary, members) {
|
|
72337
72727
|
const { id: summaryId, deviceId } = summary;
|
|
72338
72728
|
let openTracks;
|
|
72339
72729
|
try {
|
|
@@ -72346,12 +72736,13 @@ var SummarySealer = class {
|
|
|
72346
72736
|
error: errMsg(err)
|
|
72347
72737
|
}
|
|
72348
72738
|
});
|
|
72349
|
-
return;
|
|
72739
|
+
return null;
|
|
72350
72740
|
}
|
|
72351
|
-
|
|
72352
|
-
|
|
72741
|
+
const stillOpen = new Set(openTracks);
|
|
72742
|
+
if (members.some((m) => stillOpen.has(m))) return false;
|
|
72743
|
+
let parked;
|
|
72353
72744
|
try {
|
|
72354
|
-
|
|
72745
|
+
parked = await this.deps.parkedTrackIds(deviceId);
|
|
72355
72746
|
} catch (err) {
|
|
72356
72747
|
this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
|
|
72357
72748
|
tags: { deviceId },
|
|
@@ -72360,10 +72751,28 @@ var SummarySealer = class {
|
|
|
72360
72751
|
error: errMsg(err)
|
|
72361
72752
|
}
|
|
72362
72753
|
});
|
|
72754
|
+
return null;
|
|
72755
|
+
}
|
|
72756
|
+
return !members.some((m) => parked.has(m));
|
|
72757
|
+
}
|
|
72758
|
+
async sealIfDone(summary) {
|
|
72759
|
+
const { id: summaryId, deviceId } = summary;
|
|
72760
|
+
let members;
|
|
72761
|
+
try {
|
|
72762
|
+
members = (await this.deps.summaries.membersOf(summaryId)).map((m) => m.trackId);
|
|
72763
|
+
} catch (err) {
|
|
72764
|
+
this.deps.logger.warn("summary sweep: could not read members — seal aborted this sweep", {
|
|
72765
|
+
tags: { deviceId },
|
|
72766
|
+
meta: {
|
|
72767
|
+
summaryId,
|
|
72768
|
+
error: errMsg(err)
|
|
72769
|
+
}
|
|
72770
|
+
});
|
|
72363
72771
|
return;
|
|
72364
72772
|
}
|
|
72365
|
-
if (
|
|
72773
|
+
if (await this.isGroupFinished(summary, members) !== true) return;
|
|
72366
72774
|
const sealedAt = this.deps.now();
|
|
72775
|
+
await this.stampLabels(summary, members);
|
|
72367
72776
|
try {
|
|
72368
72777
|
if (await this.deps.summaries.seal(summary, sealedAt)) {
|
|
72369
72778
|
const members = this.forgetGroup(deviceId, summaryId);
|
|
@@ -72396,6 +72805,37 @@ var SummarySealer = class {
|
|
|
72396
72805
|
if (members === void 0) groups.set(summaryIdValue, new Set([trackId]));
|
|
72397
72806
|
else members.add(trackId);
|
|
72398
72807
|
}
|
|
72808
|
+
/**
|
|
72809
|
+
* Write each member's final label onto its row.
|
|
72810
|
+
*
|
|
72811
|
+
* At the seal every member's track has closed, so what the pipeline decided
|
|
72812
|
+
* about it is final — this is the first and last moment the stamp can be
|
|
72813
|
+
* both complete and correct.
|
|
72814
|
+
*/
|
|
72815
|
+
async stampLabels(summary, members) {
|
|
72816
|
+
const read = this.deps.labelsFor;
|
|
72817
|
+
if (read === void 0 || members.length === 0) return;
|
|
72818
|
+
try {
|
|
72819
|
+
const labels = await read(members);
|
|
72820
|
+
for (const [trackId, value] of labels) {
|
|
72821
|
+
const label = value.label;
|
|
72822
|
+
const subLabel = value.subLabel;
|
|
72823
|
+
if ((label === void 0 || label.length === 0) && (subLabel === void 0 || subLabel.length === 0)) continue;
|
|
72824
|
+
await this.deps.summaries.stampMemberLabels(summary.id, trackId, {
|
|
72825
|
+
...label !== void 0 && label.length > 0 ? { label } : {},
|
|
72826
|
+
...subLabel !== void 0 && subLabel.length > 0 ? { subLabel } : {}
|
|
72827
|
+
});
|
|
72828
|
+
}
|
|
72829
|
+
} catch (err) {
|
|
72830
|
+
this.deps.logger.warn("summary: could not stamp member labels — the seal stands", {
|
|
72831
|
+
tags: { deviceId: summary.deviceId },
|
|
72832
|
+
meta: {
|
|
72833
|
+
summaryId: summary.id,
|
|
72834
|
+
error: errMsg(err)
|
|
72835
|
+
}
|
|
72836
|
+
});
|
|
72837
|
+
}
|
|
72838
|
+
}
|
|
72399
72839
|
/** Follows a split in the mirror, so the frame path scores each half on its
|
|
72400
72840
|
* own members from the very next frame. */
|
|
72401
72841
|
moveMirrorMembers(deviceId, fromId, toId, trackIds) {
|
|
@@ -73968,6 +74408,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73968
74408
|
await secretState.set(secret);
|
|
73969
74409
|
}
|
|
73970
74410
|
const shelfWindows = await this.pruneWindows();
|
|
74411
|
+
const index = new NcArtifactIndex({
|
|
74412
|
+
store: this.ctx.api.settingsStore,
|
|
74413
|
+
logger: this.ctx.logger.child("nc-artifacts"),
|
|
74414
|
+
addonId: this.ctx.id,
|
|
74415
|
+
retentionMs: shelfWindows.artifactShelfMs
|
|
74416
|
+
});
|
|
74417
|
+
this.ncArtifactIndex = index;
|
|
74418
|
+
const refusal = artifactShelfRefusal(await this.ctx.api.storage.listLocations.query({}), this.ctx.kernel.localNodeId ?? null);
|
|
74419
|
+
if (refusal !== null) {
|
|
74420
|
+
this.ctx.logger.info("nc-artifact shelf not served on this node", { meta: {
|
|
74421
|
+
reason: refusal,
|
|
74422
|
+
indexServed: true
|
|
74423
|
+
} });
|
|
74424
|
+
return;
|
|
74425
|
+
}
|
|
73971
74426
|
const store = new NcArtifactStore({
|
|
73972
74427
|
rootDir: await this.resolveNcArtifactRoot(),
|
|
73973
74428
|
logger: this.ctx.logger.child("nc-artifacts"),
|
|
@@ -73975,13 +74430,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73975
74430
|
});
|
|
73976
74431
|
await store.start();
|
|
73977
74432
|
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
74433
|
const plane = new NcArtifactPlane({
|
|
73986
74434
|
store,
|
|
73987
74435
|
secret,
|
|
@@ -75341,7 +75789,25 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75341
75789
|
summaries: this.summaryStore,
|
|
75342
75790
|
analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
|
|
75343
75791
|
openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
|
|
75344
|
-
|
|
75792
|
+
parkedTrackIds: async (deviceId) => {
|
|
75793
|
+
const ids = /* @__PURE__ */ new Set();
|
|
75794
|
+
for (const entry of this.stationaryRegistry?.list(deviceId) ?? []) if (entry.sourceTrackId !== void 0) ids.add(entry.sourceTrackId);
|
|
75795
|
+
return ids;
|
|
75796
|
+
},
|
|
75797
|
+
labelsFor: async (trackIds) => {
|
|
75798
|
+
const out = /* @__PURE__ */ new Map();
|
|
75799
|
+
const tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
|
|
75800
|
+
for (const track of tracks) {
|
|
75801
|
+
const label = track.label;
|
|
75802
|
+
const subLabel = track.subLabel;
|
|
75803
|
+
if (label === void 0 && subLabel === void 0) continue;
|
|
75804
|
+
out.set(track.trackId, {
|
|
75805
|
+
...label !== void 0 ? { label } : {},
|
|
75806
|
+
...subLabel !== void 0 ? { subLabel } : {}
|
|
75807
|
+
});
|
|
75808
|
+
}
|
|
75809
|
+
return out;
|
|
75810
|
+
},
|
|
75345
75811
|
now: () => Date.now(),
|
|
75346
75812
|
logger: this.ctx.logger.child("summary-sealer"),
|
|
75347
75813
|
frameSize: (deviceId) => {
|
|
@@ -76046,6 +76512,30 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76046
76512
|
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
76047
76513
|
}));
|
|
76048
76514
|
},
|
|
76515
|
+
listSummaryGroups: async ({ deviceId, sinceMs, untilMs }) => {
|
|
76516
|
+
const summaryStore = this.summaryStore;
|
|
76517
|
+
if (!summaryStore) return [];
|
|
76518
|
+
const rows = await summaryStore.listRecent({
|
|
76519
|
+
deviceIds: [deviceId],
|
|
76520
|
+
since: sinceMs,
|
|
76521
|
+
until: untilMs,
|
|
76522
|
+
limit: 500
|
|
76523
|
+
});
|
|
76524
|
+
const out = [];
|
|
76525
|
+
for (const row of rows) {
|
|
76526
|
+
const members = await summaryStore.membersOf(row.id);
|
|
76527
|
+
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|
|
76528
|
+
const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
|
|
76529
|
+
out.push({
|
|
76530
|
+
summaryId: row.id,
|
|
76531
|
+
deviceId: row.deviceId,
|
|
76532
|
+
memberTrackIds: members.map((m) => m.trackId),
|
|
76533
|
+
primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null,
|
|
76534
|
+
groupShotMediaId: shot?.mediaId ?? null
|
|
76535
|
+
});
|
|
76536
|
+
}
|
|
76537
|
+
return out;
|
|
76538
|
+
},
|
|
76049
76539
|
listMediaInfo: async (id) => {
|
|
76050
76540
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
76051
76541
|
mediaId: r.mediaId,
|
|
@@ -81194,23 +81684,96 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
81194
81684
|
deviceIds: input.deviceIds,
|
|
81195
81685
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
81196
81686
|
...input.until !== void 0 ? { until: input.until } : {},
|
|
81197
|
-
limit: input.limit ?? 50
|
|
81687
|
+
limit: input.limit ?? 50,
|
|
81688
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81198
81689
|
});
|
|
81199
|
-
return { summaries: await
|
|
81690
|
+
return { summaries: await this.listItemsFor(rows) };
|
|
81200
81691
|
}
|
|
81201
|
-
|
|
81202
|
-
|
|
81203
|
-
|
|
81692
|
+
/**
|
|
81693
|
+
* Rows → what a LIST draws: the envelope plus the composition.
|
|
81694
|
+
*
|
|
81695
|
+
* Two bulk reads for the whole page, never per row: the members in one, their
|
|
81696
|
+
* tracks (for the names) in one. The MEMBERS themselves stay off the list —
|
|
81697
|
+
* a page of two hundred groups would otherwise carry a thousand records so
|
|
81698
|
+
* the client could count them. `getSummary` is where the links live.
|
|
81699
|
+
*/
|
|
81700
|
+
async listItemsFor(rows) {
|
|
81701
|
+
const store = this.summaryStore;
|
|
81702
|
+
if (!store || rows.length === 0) return [];
|
|
81703
|
+
const members = await store.membersOfMany(rows.map((r) => r.id));
|
|
81704
|
+
return await Promise.all(rows.map(async (row) => ({
|
|
81705
|
+
summary: await this.toSummary(row),
|
|
81706
|
+
...summaryComposition(members.get(row.id) ?? [])
|
|
81707
|
+
})));
|
|
81708
|
+
}
|
|
81709
|
+
/** The members of a page of summaries, with the labels a card can show —
|
|
81710
|
+
* in ONE read of the members and ONE of their tracks. */
|
|
81711
|
+
async labelledMembersFor(rows) {
|
|
81712
|
+
const out = /* @__PURE__ */ new Map();
|
|
81713
|
+
const store = this.summaryStore;
|
|
81714
|
+
if (!store || rows.length === 0) return out;
|
|
81715
|
+
const membersBySummary = await store.membersOfMany(rows.map((r) => r.id));
|
|
81716
|
+
const trackIds = [...new Set([...membersBySummary.values()].flatMap((ms) => ms.map((m) => m.trackId)))];
|
|
81717
|
+
let tracks = [];
|
|
81718
|
+
try {
|
|
81719
|
+
tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
|
|
81720
|
+
} catch (err) {
|
|
81721
|
+
this.ctx.logger.warn("summary members: labels unavailable for this page", { meta: {
|
|
81722
|
+
summaries: rows.length,
|
|
81723
|
+
members: trackIds.length,
|
|
81724
|
+
error: errMsg$1(err)
|
|
81725
|
+
} });
|
|
81726
|
+
}
|
|
81727
|
+
for (const row of rows) out.set(row.id, withMemberLabels(membersBySummary.get(row.id) ?? [], tracks));
|
|
81728
|
+
return out;
|
|
81729
|
+
}
|
|
81730
|
+
/**
|
|
81731
|
+
* Rows → the shape every summary surface answers with: the envelope, its
|
|
81732
|
+
* members, and what a thumbnail can render for each of them.
|
|
81733
|
+
*
|
|
81734
|
+
* THREE reads for the whole page, never per row: the members in one, their
|
|
81735
|
+
* tracks in one, and the group shots through `toSummary`. A read per member
|
|
81736
|
+
* is what made a sixty-group page take twenty seconds on the events feed.
|
|
81737
|
+
*/
|
|
81738
|
+
/** ONE summary, with every member and the link each of them carries. */
|
|
81739
|
+
async detailFor(row) {
|
|
81740
|
+
const labelled = (await this.labelledMembersFor([row])).get(row.id) ?? [];
|
|
81204
81741
|
return {
|
|
81205
|
-
summary: await this.toSummary(
|
|
81206
|
-
members:
|
|
81742
|
+
summary: await this.toSummary(row),
|
|
81743
|
+
members: labelled.map((m) => ({
|
|
81207
81744
|
trackId: m.trackId,
|
|
81208
81745
|
className: m.className,
|
|
81209
81746
|
role: m.role,
|
|
81210
|
-
joinedAt: m.joinedAt
|
|
81747
|
+
joinedAt: m.joinedAt,
|
|
81748
|
+
...m.label !== void 0 ? { label: m.label } : {},
|
|
81749
|
+
...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
|
|
81211
81750
|
}))
|
|
81212
81751
|
};
|
|
81213
81752
|
}
|
|
81753
|
+
async listRecentSummaries(input) {
|
|
81754
|
+
const store = this.summaryStore;
|
|
81755
|
+
if (!store) return {
|
|
81756
|
+
summaries: [],
|
|
81757
|
+
nextCursor: null
|
|
81758
|
+
};
|
|
81759
|
+
const page = await store.queryRecent({
|
|
81760
|
+
deviceIds: input.deviceIds,
|
|
81761
|
+
...input.since !== void 0 ? { since: input.since } : {},
|
|
81762
|
+
...input.until !== void 0 ? { until: input.until } : {},
|
|
81763
|
+
limit: input.limit,
|
|
81764
|
+
...input.cursor !== void 0 ? { cursor: input.cursor } : {},
|
|
81765
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81766
|
+
});
|
|
81767
|
+
return {
|
|
81768
|
+
summaries: await this.listItemsFor(page.summaries.map((d) => d.summary)),
|
|
81769
|
+
nextCursor: page.nextCursor
|
|
81770
|
+
};
|
|
81771
|
+
}
|
|
81772
|
+
async getSummary(input) {
|
|
81773
|
+
const detail = await this.summaryStore?.get(input.id);
|
|
81774
|
+
if (!detail) return null;
|
|
81775
|
+
return await this.detailFor(detail.summary);
|
|
81776
|
+
}
|
|
81214
81777
|
/** The row plus its group shot: the web companion when it landed, else the native key frame. */
|
|
81215
81778
|
async toSummary(row) {
|
|
81216
81779
|
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|