@camstack/addon-post-analysis 1.2.230 → 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 +609 -43
- package/dist/pipeline-analytics/index.mjs +609 -43
- 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.
|
|
@@ -72246,7 +72622,9 @@ var SummarySealer = class {
|
|
|
72246
72622
|
if (this.sweepInFlight) return;
|
|
72247
72623
|
this.sweepInFlight = true;
|
|
72248
72624
|
try {
|
|
72249
|
-
const
|
|
72625
|
+
const mine = this.deps.analysedDeviceIds();
|
|
72626
|
+
const open = [];
|
|
72627
|
+
for (const deviceId of mine) open.push(...await this.deps.summaries.listOpen(deviceId));
|
|
72250
72628
|
for (const summary of open) {
|
|
72251
72629
|
const born = await this.splitIfApart(summary);
|
|
72252
72630
|
await this.sealIfDone(summary);
|
|
@@ -72331,7 +72709,21 @@ var SummarySealer = class {
|
|
|
72331
72709
|
return null;
|
|
72332
72710
|
}
|
|
72333
72711
|
}
|
|
72334
|
-
|
|
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) {
|
|
72335
72727
|
const { id: summaryId, deviceId } = summary;
|
|
72336
72728
|
let openTracks;
|
|
72337
72729
|
try {
|
|
@@ -72344,12 +72736,13 @@ var SummarySealer = class {
|
|
|
72344
72736
|
error: errMsg(err)
|
|
72345
72737
|
}
|
|
72346
72738
|
});
|
|
72347
|
-
return;
|
|
72739
|
+
return null;
|
|
72348
72740
|
}
|
|
72349
|
-
|
|
72350
|
-
|
|
72741
|
+
const stillOpen = new Set(openTracks);
|
|
72742
|
+
if (members.some((m) => stillOpen.has(m))) return false;
|
|
72743
|
+
let parked;
|
|
72351
72744
|
try {
|
|
72352
|
-
|
|
72745
|
+
parked = await this.deps.parkedTrackIds(deviceId);
|
|
72353
72746
|
} catch (err) {
|
|
72354
72747
|
this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
|
|
72355
72748
|
tags: { deviceId },
|
|
@@ -72358,10 +72751,28 @@ var SummarySealer = class {
|
|
|
72358
72751
|
error: errMsg(err)
|
|
72359
72752
|
}
|
|
72360
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
|
+
});
|
|
72361
72771
|
return;
|
|
72362
72772
|
}
|
|
72363
|
-
if (
|
|
72773
|
+
if (await this.isGroupFinished(summary, members) !== true) return;
|
|
72364
72774
|
const sealedAt = this.deps.now();
|
|
72775
|
+
await this.stampLabels(summary, members);
|
|
72365
72776
|
try {
|
|
72366
72777
|
if (await this.deps.summaries.seal(summary, sealedAt)) {
|
|
72367
72778
|
const members = this.forgetGroup(deviceId, summaryId);
|
|
@@ -72394,6 +72805,37 @@ var SummarySealer = class {
|
|
|
72394
72805
|
if (members === void 0) groups.set(summaryIdValue, new Set([trackId]));
|
|
72395
72806
|
else members.add(trackId);
|
|
72396
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
|
+
}
|
|
72397
72839
|
/** Follows a split in the mirror, so the frame path scores each half on its
|
|
72398
72840
|
* own members from the very next frame. */
|
|
72399
72841
|
moveMirrorMembers(deviceId, fromId, toId, trackIds) {
|
|
@@ -73966,6 +74408,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73966
74408
|
await secretState.set(secret);
|
|
73967
74409
|
}
|
|
73968
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
|
+
}
|
|
73969
74426
|
const store = new NcArtifactStore({
|
|
73970
74427
|
rootDir: await this.resolveNcArtifactRoot(),
|
|
73971
74428
|
logger: this.ctx.logger.child("nc-artifacts"),
|
|
@@ -73973,13 +74430,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73973
74430
|
});
|
|
73974
74431
|
await store.start();
|
|
73975
74432
|
this.ncArtifactStore = store;
|
|
73976
|
-
const index = new NcArtifactIndex({
|
|
73977
|
-
store: this.ctx.api.settingsStore,
|
|
73978
|
-
logger: this.ctx.logger.child("nc-artifacts"),
|
|
73979
|
-
addonId: this.ctx.id,
|
|
73980
|
-
retentionMs: store.retentionMs
|
|
73981
|
-
});
|
|
73982
|
-
this.ncArtifactIndex = index;
|
|
73983
74433
|
const plane = new NcArtifactPlane({
|
|
73984
74434
|
store,
|
|
73985
74435
|
secret,
|
|
@@ -75337,8 +75787,27 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
75337
75787
|
});
|
|
75338
75788
|
this.summarySealer = new SummarySealer({
|
|
75339
75789
|
summaries: this.summaryStore,
|
|
75790
|
+
analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
|
|
75340
75791
|
openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
|
|
75341
|
-
|
|
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
|
+
},
|
|
75342
75811
|
now: () => Date.now(),
|
|
75343
75812
|
logger: this.ctx.logger.child("summary-sealer"),
|
|
75344
75813
|
frameSize: (deviceId) => {
|
|
@@ -76043,6 +76512,30 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
76043
76512
|
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
76044
76513
|
}));
|
|
76045
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
|
+
},
|
|
76046
76539
|
listMediaInfo: async (id) => {
|
|
76047
76540
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
76048
76541
|
mediaId: r.mediaId,
|
|
@@ -81191,23 +81684,96 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
81191
81684
|
deviceIds: input.deviceIds,
|
|
81192
81685
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
81193
81686
|
...input.until !== void 0 ? { until: input.until } : {},
|
|
81194
|
-
limit: input.limit ?? 50
|
|
81687
|
+
limit: input.limit ?? 50,
|
|
81688
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81195
81689
|
});
|
|
81196
|
-
return { summaries: await
|
|
81690
|
+
return { summaries: await this.listItemsFor(rows) };
|
|
81197
81691
|
}
|
|
81198
|
-
|
|
81199
|
-
|
|
81200
|
-
|
|
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) ?? [];
|
|
81201
81741
|
return {
|
|
81202
|
-
summary: await this.toSummary(
|
|
81203
|
-
members:
|
|
81742
|
+
summary: await this.toSummary(row),
|
|
81743
|
+
members: labelled.map((m) => ({
|
|
81204
81744
|
trackId: m.trackId,
|
|
81205
81745
|
className: m.className,
|
|
81206
81746
|
role: m.role,
|
|
81207
|
-
joinedAt: m.joinedAt
|
|
81747
|
+
joinedAt: m.joinedAt,
|
|
81748
|
+
...m.label !== void 0 ? { label: m.label } : {},
|
|
81749
|
+
...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
|
|
81208
81750
|
}))
|
|
81209
81751
|
};
|
|
81210
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
|
+
}
|
|
81211
81777
|
/** The row plus its group shot: the web companion when it landed, else the native key frame. */
|
|
81212
81778
|
async toSummary(row) {
|
|
81213
81779
|
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|