@camstack/addon-post-analysis 1.2.296 → 1.2.298
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/{clip-model-registry-BP_o2Oi4.mjs → clip-model-registry-D9WaswEs.mjs} +1 -164
- package/dist/{clip-model-registry-BTpYUJzK.js → clip-model-registry-olxNKN7T.js} +1 -164
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C9vZfpmu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bk7ACano.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BVgWHpfo.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1CfMkm2.mjs} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-KXwf-qxG.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Ckt8VrEv.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BwZmL8b9.mjs → hostInit-DtPqVNsM.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +284 -2615
- package/dist/pipeline-analytics/index.mjs +347 -2678
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as NcRuleSchema, $n as string, $t as isSystemEventSummaryRule, A as LockStateSchema, An as evaluateSensorEdge, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bn as isDeviceScopedCap, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Cn as systemEventSummaryScheduleOf, Ct as TimelapseRuleInputSchema, D as FULL_IMAGE_BBOX, Dn as zoneAnalyticsCapability, Dt as addonWidgetsSourceCapability, E as FIRST_LEVEL_MACRO_CLASSES, En as vectorDimFromBase64, Et as TrackSourceSchema, F as MediaFileKindEnum, Fn as CamProfileSchema, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as array, Gt as faceGalleryCapability, I as MotionTriggeredBySchema, In as createEvent, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as literal, Jt as isEventOwnerType, K as NC_TEMPLATE_VARS, Kn as boolean, Kt as failureContributionCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as customAction, Lt as describeSystemEventFamilyProblem, M as MAX_BIRTH_DECISION_RECORDS, Mn as DeviceType, Mt as buildEventKindDescriptor, Nn as errMsg
|
|
1
|
+
import { $ as NcRuleSchema, $n as string, $t as isSystemEventSummaryRule, A as LockStateSchema, An as evaluateSensorEdge, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bn as isDeviceScopedCap, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Cn as systemEventSummaryScheduleOf, Ct as TimelapseRuleInputSchema, D as FULL_IMAGE_BBOX, Dn as zoneAnalyticsCapability, Dt as addonWidgetsSourceCapability, E as FIRST_LEVEL_MACRO_CLASSES, En as vectorDimFromBase64, Et as TrackSourceSchema, F as MediaFileKindEnum, Fn as CamProfileSchema, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as array, Gt as faceGalleryCapability, I as MotionTriggeredBySchema, In as createEvent, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as literal, Jt as isEventOwnerType, K as NC_TEMPLATE_VARS, Kn as boolean, Kt as failureContributionCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as customAction, Lt as describeSystemEventFamilyProblem, M as MAX_BIRTH_DECISION_RECORDS, Mn as DeviceType, Mt as buildEventKindDescriptor, Nn as errMsg, Nt as consumableLowPercentOf, O as FailureCounters, Ot as alarmPanelCapability, P as MOTION_CLOSE_AFTER_MS, Pn as BaseAddon, Pt as cosineSimilarity$1, Q as NcRulePatchSchema, Qn as record, Qt as isSystemDelivery, R as NC_CONDITION_CATALOG, Rn as defineCustomActions, S as EMBEDDING_REQUIREMENTS_FAILED, Sn as systemEventFilterApplies, St as TIMELAPSE_DENSE_FLOOR_SEC, T as EVENT_OWNER_TYPES, Tn as templateVarsFor, Tt as TimelapseRuleSchema, U as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Un as sleep$1, Ut as encodeVectorBase64, V as NC_SENSOR_TRANSITION_KINDS, Vn as nodePin, W as NC_TANK_SYSTEM_EVENT_KINDS, Wn as _enum, Wt as evictionPolicyOfLocation, X as NcConditionsSchema, Xn as object, Xt as isMediaOwnerType, Y as NcConditionDescriptorSchema, Yn as number, Yt as isKnownTemplateVar, Z as NcRuleInputSchema, Zn as partialRecord, Zt as isScheduleActive, _n as servedTemplateVars, a as CLIP_EMBEDDING_STEP_ID, at as NcSnoozeSuppressedSchema, b as DeclaredDevices, bn as systemEventDeliveryProblems, bt as SCENE_DIVERGED, c as resolveClusterModelPin, cn as readDeviceStateFrom, ct as NcTemplatePreviewInputSchema, d as ArchivedDebugNoteSchema, dn as resolveClipSearchMinScore, dt as OpsLogEntrySchema, en as isVectorIndexDimConflict, er as unknown, et as NcRuleTargetSchema, f as BaseDevice, fn as resolveLocationMode, g as DEFAULT_EVENT_COLOR, gn as sensorRaisedOf, gt as RetrainStatusSchema, h as CoverStateSchema, hn as sceneMonitorCapability, ht as RebuildObjectEmbeddingsRefusalSchema, in as occupancyScope, it as NcSnoozeSchema, j as MAX_ARCHIVED_DEBUG_NOTES, jn as isSourceCap, jt as audioModeOf, k as LabelAttributionSchema, kn as canonicalHash, kt as assertTimelapseCadences, l as AUDIO_MACRO_LABELS, ln as readMotionTriggeredBy, lt as NcTemplatePreviewSchema, m as COCO_TO_MACRO, mn as ruleBelongsToAlarm$1, mt as RECORDING_SOURCE_CAMSTACK_ADDON, nn as mayWriteToLocation, nt as NcScheduleSchema, o as FACE_EMBEDDING_STEP_ID, on as pipelineAnalyticsCapability, ot as NcSystemEventKindSchema, p as BirthDecisionRecordSchema, pt as RECORDING_EXPORT_MAX_READ_BYTES, q as NcAlarmConfigSchema, qn as discriminatedUnion, qt as isDetectionMacroClass, rn as notificationRulesCapability, rt as NcSnoozeInputSchema, s as readClusterModelPinStrict, sn as plateGalleryCapability, st as NcTaxonomySchema, t as BUILTIN_CLIP_MODELS, tn as kebabToCamel, tr as EventCategory, tt as NcRuleUpdateInputSchema, u as AlarmArmModeSchema, un as readTimelapseGeneratedAt, ut as NcTemplateVarDescriptorSchema, v as DETECTION_MACRO_CLASSES, vn as storageOccupancyCapability, vt as SCENE_DEFAULT_ANCHOR_THRESHOLD, w as EVENT_KIND_BY_CAP, wn as templateCarriesSummary, wt as TimelapseRulePatchSchema, x as EMBEDDING_MODEL_FAILED, xn as systemEventFamilyProblems, xt as SceneMonitorSchema, yn as subKindsOf, yt as SCENE_DEFAULT_UNCOVERED_POLICY, z as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, zn as hydrateSchema } from "../clip-model-registry-D9WaswEs.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";
|
|
@@ -2708,8 +2708,8 @@ var NC_ARTIFACTS_INDEXES = [
|
|
|
2708
2708
|
}
|
|
2709
2709
|
];
|
|
2710
2710
|
/** Upper bound for a half-open `whereBetween` — the store has no `>=`. */
|
|
2711
|
-
var TIME_MAX
|
|
2712
|
-
var TIME_MIN
|
|
2711
|
+
var TIME_MAX = 864e13;
|
|
2712
|
+
var TIME_MIN = -864e13;
|
|
2713
2713
|
/** Prune scan ceiling — the index is bounded by the shelf it mirrors (2000
|
|
2714
2714
|
* entries by default), so this only has to be comfortably above it. */
|
|
2715
2715
|
var NC_ARTIFACT_PRUNE_SCAN_LIMIT = 2e4;
|
|
@@ -2949,7 +2949,7 @@ var NcArtifactIndex = class {
|
|
|
2949
2949
|
try {
|
|
2950
2950
|
const expired = await this.store.deleteWhere.mutate({
|
|
2951
2951
|
collection: NC_ARTIFACTS_COLLECTION,
|
|
2952
|
-
filter: { whereBetween: { expiresAt: [TIME_MIN
|
|
2952
|
+
filter: { whereBetween: { expiresAt: [TIME_MIN, this.now() - 1] } }
|
|
2953
2953
|
});
|
|
2954
2954
|
removed += expired.deleted;
|
|
2955
2955
|
if (keptIds === null) {
|
|
@@ -2991,8 +2991,8 @@ var NcArtifactIndex = class {
|
|
|
2991
2991
|
whereIn["kind"] = query.kinds !== void 0 && query.kinds.length > 0 ? [...query.kinds] : [...NC_GALLERY_ARTIFACT_KINDS];
|
|
2992
2992
|
if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
|
|
2993
2993
|
const whereBetween = {
|
|
2994
|
-
capturedAt: [query.fromMs ?? TIME_MIN
|
|
2995
|
-
expiresAt: [this.now(), TIME_MAX
|
|
2994
|
+
capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
|
|
2995
|
+
expiresAt: [this.now(), TIME_MAX]
|
|
2996
2996
|
};
|
|
2997
2997
|
return {
|
|
2998
2998
|
...query.ruleId !== void 0 ? { where: { ruleId: query.ruleId } } : {},
|
|
@@ -15434,21 +15434,6 @@ var NcSummaryRuleInputSchema = object({
|
|
|
15434
15434
|
deviceIds: DeviceIdsField,
|
|
15435
15435
|
window: NcSummaryWindowSchema,
|
|
15436
15436
|
filters: NcSummaryFiltersSchema.optional(),
|
|
15437
|
-
/**
|
|
15438
|
-
* What ONE line of this digest is: a subject, or a group of subjects that
|
|
15439
|
-
* arrived together.
|
|
15440
|
-
*
|
|
15441
|
-
* `tracks` is what the digest has always done and stays the default — the
|
|
15442
|
-
* operator's instruction when groups were added was that the first behaviour
|
|
15443
|
-
* remains. `groups` collapses the members of each session summary to one
|
|
15444
|
-
* entry, so a family walking in is one line and not four
|
|
15445
|
-
* (`summary-group-collapse.ts`).
|
|
15446
|
-
*
|
|
15447
|
-
* `.default('tracks')` is safe here where `NcSummaryFiltersSchema` fields are
|
|
15448
|
-
* not: this is a field of the RULE, not of the partial `filters` object that
|
|
15449
|
-
* a patch materialises wholesale.
|
|
15450
|
-
*/
|
|
15451
|
-
subject: _enum(["tracks", "groups"]).default("tracks"),
|
|
15452
15437
|
maxTiles: MaxTilesField.default(12),
|
|
15453
15438
|
maxAiImages: MaxAiImagesField.default(6),
|
|
15454
15439
|
rank: NcSummaryRankSchema.default("importance"),
|
|
@@ -15483,7 +15468,6 @@ var NcSummaryRulePatchSchema = object({
|
|
|
15483
15468
|
maxTiles: MaxTilesField.optional(),
|
|
15484
15469
|
maxAiImages: MaxAiImagesField.optional(),
|
|
15485
15470
|
rank: NcSummaryRankSchema.optional(),
|
|
15486
|
-
subject: _enum(["tracks", "groups"]).optional(),
|
|
15487
15471
|
barrierSeconds: BarrierSecondsField.optional(),
|
|
15488
15472
|
targets: TargetsField.optional(),
|
|
15489
15473
|
template: NcSummaryTemplateSchema.nullable().optional(),
|
|
@@ -26277,52 +26261,6 @@ function buildSummaryOutboxInputs(input) {
|
|
|
26277
26261
|
});
|
|
26278
26262
|
}
|
|
26279
26263
|
//#endregion
|
|
26280
|
-
//#region src/notification-center/summary/summary-group-collapse.ts
|
|
26281
|
-
function collapseToGroups(candidates, groups) {
|
|
26282
|
-
const claim = /* @__PURE__ */ new Map();
|
|
26283
|
-
for (const group of groups) for (const trackId of group.memberTrackIds) if (!claim.has(trackId)) claim.set(trackId, group);
|
|
26284
|
-
const survivorsByGroup = /* @__PURE__ */ new Map();
|
|
26285
|
-
for (const candidate of candidates) {
|
|
26286
|
-
const group = claim.get(candidate.trackId);
|
|
26287
|
-
if (group === void 0) continue;
|
|
26288
|
-
const bucket = survivorsByGroup.get(group.summaryId);
|
|
26289
|
-
if (bucket === void 0) survivorsByGroup.set(group.summaryId, [candidate]);
|
|
26290
|
-
else bucket.push(candidate);
|
|
26291
|
-
}
|
|
26292
|
-
const representative = /* @__PURE__ */ new Map();
|
|
26293
|
-
for (const [summaryId, survivors] of survivorsByGroup) {
|
|
26294
|
-
const group = groups.find((g) => g.summaryId === summaryId);
|
|
26295
|
-
if (group === void 0) continue;
|
|
26296
|
-
const chosen = survivors.find((c) => c.trackId === group.primaryTrackId) ?? survivors[0];
|
|
26297
|
-
if (chosen === void 0) continue;
|
|
26298
|
-
representative.set(summaryId, {
|
|
26299
|
-
...chosen,
|
|
26300
|
-
groupSize: survivors.length,
|
|
26301
|
-
...group.groupShotMediaId !== null ? { groupShotMediaId: group.groupShotMediaId } : {}
|
|
26302
|
-
});
|
|
26303
|
-
}
|
|
26304
|
-
const kept = [];
|
|
26305
|
-
let collapsed = 0;
|
|
26306
|
-
for (const candidate of candidates) {
|
|
26307
|
-
const group = claim.get(candidate.trackId);
|
|
26308
|
-
if (group === void 0) {
|
|
26309
|
-
kept.push(candidate);
|
|
26310
|
-
continue;
|
|
26311
|
-
}
|
|
26312
|
-
const chosen = representative.get(group.summaryId);
|
|
26313
|
-
if (chosen === void 0) {
|
|
26314
|
-
kept.push(candidate);
|
|
26315
|
-
continue;
|
|
26316
|
-
}
|
|
26317
|
-
if (chosen.trackId === candidate.trackId) kept.push(chosen);
|
|
26318
|
-
else collapsed += 1;
|
|
26319
|
-
}
|
|
26320
|
-
return {
|
|
26321
|
-
kept,
|
|
26322
|
-
collapsed
|
|
26323
|
-
};
|
|
26324
|
-
}
|
|
26325
|
-
//#endregion
|
|
26326
26264
|
//#region src/notification-center/summary/summary-selection.ts
|
|
26327
26265
|
/**
|
|
26328
26266
|
* Which tracks become tiles — filter, rank, and the per-camera quota.
|
|
@@ -26916,8 +26854,7 @@ var NcSummaryProducer = class {
|
|
|
26916
26854
|
try {
|
|
26917
26855
|
const candidates = await this.collectCandidates(rule, window);
|
|
26918
26856
|
const filtered = filterSummaryCandidates(candidates, rule.filters, this.deps.zoneOwner);
|
|
26919
|
-
const
|
|
26920
|
-
const selection = selectSummaryTiles(grouped.kept, {
|
|
26857
|
+
const selection = selectSummaryTiles(filtered.kept, {
|
|
26921
26858
|
maxTiles: rule.maxTiles,
|
|
26922
26859
|
rank: rule.rank
|
|
26923
26860
|
});
|
|
@@ -26946,7 +26883,7 @@ var NcSummaryProducer = class {
|
|
|
26946
26883
|
const tileLoad = await this.loadTiles({
|
|
26947
26884
|
rule,
|
|
26948
26885
|
preferred: selection.tiles,
|
|
26949
|
-
refill: remainingSummaryCandidates(
|
|
26886
|
+
refill: remainingSummaryCandidates(filtered.kept, selection.tiles, rule.rank),
|
|
26950
26887
|
maxTiles: rule.maxTiles
|
|
26951
26888
|
});
|
|
26952
26889
|
const loaded = tileLoad.tiles;
|
|
@@ -26987,7 +26924,7 @@ var NcSummaryProducer = class {
|
|
|
26987
26924
|
priority: rule.priority,
|
|
26988
26925
|
tileCount: loaded.length,
|
|
26989
26926
|
cameraCount: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
26990
|
-
matchedCount:
|
|
26927
|
+
matchedCount: filtered.kept.length,
|
|
26991
26928
|
detections,
|
|
26992
26929
|
...mosaic.ref !== null ? { mosaic: mosaic.ref } : {},
|
|
26993
26930
|
generatedAt: this.deps.now(),
|
|
@@ -26999,7 +26936,7 @@ var NcSummaryProducer = class {
|
|
|
26999
26936
|
this.deps.logger.info("summary produced", { meta: {
|
|
27000
26937
|
...meta,
|
|
27001
26938
|
scanned: candidates.length,
|
|
27002
|
-
matched:
|
|
26939
|
+
matched: filtered.kept.length,
|
|
27003
26940
|
droppedStationary: filtered.droppedStationary,
|
|
27004
26941
|
droppedUnconfirmed: filtered.droppedUnconfirmed,
|
|
27005
26942
|
unknownConfirmation: filtered.unknownConfirmation,
|
|
@@ -27022,7 +26959,7 @@ var NcSummaryProducer = class {
|
|
|
27022
26959
|
window,
|
|
27023
26960
|
tiles: loaded.length,
|
|
27024
26961
|
cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
27025
|
-
matched:
|
|
26962
|
+
matched: filtered.kept.length,
|
|
27026
26963
|
droppedStationary: filtered.droppedStationary,
|
|
27027
26964
|
enqueued,
|
|
27028
26965
|
...mosaic.bytes > 0 ? { mosaicBytes: mosaic.bytes } : {},
|
|
@@ -27043,54 +26980,6 @@ var NcSummaryProducer = class {
|
|
|
27043
26980
|
};
|
|
27044
26981
|
}
|
|
27045
26982
|
}
|
|
27046
|
-
/**
|
|
27047
|
-
* Fold each session summary's surviving members into one entry — or do
|
|
27048
|
-
* nothing at all, which is what `tracks` (the default) means.
|
|
27049
|
-
*
|
|
27050
|
-
* A failure to read the groups does NOT fail the digest: it falls back to
|
|
27051
|
-
* subjects and says so. A digest of four lines instead of one is a worse
|
|
27052
|
-
* reading, not a lost one, and losing the night's record over a store read
|
|
27053
|
-
* is the wrong trade.
|
|
27054
|
-
*/
|
|
27055
|
-
async collapseIfGrouping(rule, window, kept) {
|
|
27056
|
-
if (rule.subject !== "groups") return {
|
|
27057
|
-
kept,
|
|
27058
|
-
collapsed: 0
|
|
27059
|
-
};
|
|
27060
|
-
const listGroups = this.deps.listSummaryGroups;
|
|
27061
|
-
if (listGroups === void 0) {
|
|
27062
|
-
this.deps.logger.warn("summary rule asks for groups but this node cannot read them", { meta: { ruleId: rule.id } });
|
|
27063
|
-
return {
|
|
27064
|
-
kept,
|
|
27065
|
-
collapsed: 0
|
|
27066
|
-
};
|
|
27067
|
-
}
|
|
27068
|
-
const groups = [];
|
|
27069
|
-
for (const deviceId of rule.deviceIds) try {
|
|
27070
|
-
groups.push(...await listGroups({
|
|
27071
|
-
deviceId,
|
|
27072
|
-
sinceMs: window.startMs,
|
|
27073
|
-
untilMs: window.endMs
|
|
27074
|
-
}));
|
|
27075
|
-
} catch (err) {
|
|
27076
|
-
this.deps.logger.warn("summary could not read a camera’s groups — read as subjects", {
|
|
27077
|
-
tags: { deviceId },
|
|
27078
|
-
meta: {
|
|
27079
|
-
ruleId: rule.id,
|
|
27080
|
-
error: String(err)
|
|
27081
|
-
}
|
|
27082
|
-
});
|
|
27083
|
-
}
|
|
27084
|
-
const outcome = collapseToGroups(kept, groups);
|
|
27085
|
-
this.deps.logger.info("summary read as groups", { meta: {
|
|
27086
|
-
ruleId: rule.id,
|
|
27087
|
-
groups: groups.length,
|
|
27088
|
-
subjects: kept.length,
|
|
27089
|
-
lines: outcome.kept.length,
|
|
27090
|
-
collapsed: outcome.collapsed
|
|
27091
|
-
} });
|
|
27092
|
-
return outcome;
|
|
27093
|
-
}
|
|
27094
26983
|
/** Every camera's tracks in the window, capped per camera and counted. */
|
|
27095
26984
|
async collectCandidates(rule, window) {
|
|
27096
26985
|
const all = [];
|
|
@@ -27141,25 +27030,6 @@ var NcSummaryProducer = class {
|
|
|
27141
27030
|
seenIds.add(candidate.trackId);
|
|
27142
27031
|
const tags = { deviceId: candidate.deviceId };
|
|
27143
27032
|
try {
|
|
27144
|
-
const groupShot = candidate.groupShotMediaId;
|
|
27145
|
-
if (groupShot !== void 0) {
|
|
27146
|
-
const jpeg = await this.deps.readMedia(groupShot);
|
|
27147
|
-
if (jpeg !== null && jpeg.byteLength > 0) {
|
|
27148
|
-
tiles.push({
|
|
27149
|
-
candidate,
|
|
27150
|
-
jpeg
|
|
27151
|
-
});
|
|
27152
|
-
continue;
|
|
27153
|
-
}
|
|
27154
|
-
this.deps.logger.info("summary tile: group shot unreadable — using a member", {
|
|
27155
|
-
tags,
|
|
27156
|
-
meta: {
|
|
27157
|
-
ruleId: input.rule.id,
|
|
27158
|
-
trackId: candidate.trackId,
|
|
27159
|
-
mediaId: groupShot
|
|
27160
|
-
}
|
|
27161
|
-
});
|
|
27162
|
-
}
|
|
27163
27033
|
const rows = await this.deps.listMediaInfo(candidate.trackId);
|
|
27164
27034
|
const picked = pickSummaryMedia(rows, (candidate.firstSeen + candidate.lastSeen) / 2);
|
|
27165
27035
|
if (picked === null) {
|
|
@@ -35435,7 +35305,6 @@ var StoragePressureTracker = class {
|
|
|
35435
35305
|
*/
|
|
35436
35306
|
var OWNER_KINDS = [
|
|
35437
35307
|
"track",
|
|
35438
|
-
"summary",
|
|
35439
35308
|
"face",
|
|
35440
35309
|
"identity",
|
|
35441
35310
|
"plate",
|
|
@@ -35497,10 +35366,19 @@ function isWriterBoundedOwnerKind(value) {
|
|
|
35497
35366
|
* `{ kind: 'retired-owner' }` and the orphan audit reclaims it under the same
|
|
35498
35367
|
* grace window and ceilings as any other orphan.
|
|
35499
35368
|
*
|
|
35369
|
+
* `'summary'` is the same case one entity later: the session summary (the
|
|
35370
|
+
* proximity group, D489/D490) was deleted on 2026-09-29 (D680) with
|
|
35371
|
+
* `pipeline-analytics:summaries` / `:summary-members`. Its group shots were
|
|
35372
|
+
* `summary`-owned media rows; they are reclaimed the same way.
|
|
35373
|
+
*
|
|
35500
35374
|
* Deliberately NOT a member of {@link OWNER_KINDS}: the type is what stops a
|
|
35501
35375
|
* new writer, and this list is only what the READ side must still recognise.
|
|
35502
35376
|
*/
|
|
35503
|
-
var RETIRED_OWNER_KINDS = [
|
|
35377
|
+
var RETIRED_OWNER_KINDS = [
|
|
35378
|
+
"group",
|
|
35379
|
+
"summary",
|
|
35380
|
+
"event"
|
|
35381
|
+
];
|
|
35504
35382
|
/** Is this an owner kind whose owning code is gone? See {@link RETIRED_OWNER_KINDS}. */
|
|
35505
35383
|
function isRetiredMediaOwnerKind(value) {
|
|
35506
35384
|
return RETIRED_OWNER_KINDS.includes(value);
|
|
@@ -42869,6 +42747,33 @@ var FaceSourceStats = class {
|
|
|
42869
42747
|
};
|
|
42870
42748
|
//#endregion
|
|
42871
42749
|
//#region src/pipeline-analytics/pipeline/key-frame-capture.ts
|
|
42750
|
+
/**
|
|
42751
|
+
* Pure helpers for the per-track NATIVE-resolution `keyFrame` capture
|
|
42752
|
+
* (Design B — one native full-frame per track at its best-detection moment).
|
|
42753
|
+
*
|
|
42754
|
+
* ## Why this exists (the missing native keyFrame)
|
|
42755
|
+
*
|
|
42756
|
+
* `keyFrame` was historically captured ONLY inside the CLIP object-embedding
|
|
42757
|
+
* best path (`persistObjectEmbeddingBests`, gated by `isClipObjectEmbedding`).
|
|
42758
|
+
* Under the two-plane pipeline the root frame carries NO CLIP embedding (clip is
|
|
42759
|
+
* a per-track DETAIL served via `runDetailSubtree`, and is disabled cluster-
|
|
42760
|
+
* wide), so that gate was never satisfied and the native `keyFrame` was NEVER
|
|
42761
|
+
* produced — every stored frame stayed at the ≤640×360 detection resolution.
|
|
42762
|
+
*
|
|
42763
|
+
* The fix decouples the `keyFrame` from the clip path: it is selected on the
|
|
42764
|
+
* GENERAL best-frame signal (the same `bestThumbnail` decision that drives the
|
|
42765
|
+
* `thumbnail`) by {@link selectKeyFrameCaptures}.
|
|
42766
|
+
*
|
|
42767
|
+
* ## How the pixels arrive, since D422
|
|
42768
|
+
*
|
|
42769
|
+
* Not by handle. At the best instant the owning node PARKS the whole frame at
|
|
42770
|
+
* {@link KEYFRAME_NATIVE_MAX_WIDTH} (`track-park.ts` orders it; the runner
|
|
42771
|
+
* refuses a ≤640 `ram-fullframe` answer at park time), and `persistKeyFrames`
|
|
42772
|
+
* stores that parcel verbatim as `keyFrame` (`key-frame-persist.ts`). The web
|
|
42773
|
+
* companion is {@link deriveKeyFrameSmall} of the same bytes — never a second
|
|
42774
|
+
* fetch. A retrieval keyed by a track never falls back to a handle
|
|
42775
|
+
* (`scripts/check-parked-frame-no-handle-fallback.ts`).
|
|
42776
|
+
*/
|
|
42872
42777
|
/** Cap (px) on the width of the native KEY FRAME (full-frame native capture).
|
|
42873
42778
|
* Native resolution is the point: a 4K cam (3840) resolves TRUE native (no
|
|
42874
42779
|
* resize, 3840 ≤ 4096); an 8K cam is bounded to 4096 (still far past the retired
|
|
@@ -42894,47 +42799,10 @@ function selectKeyFrameCaptures(targets) {
|
|
|
42894
42799
|
return selected;
|
|
42895
42800
|
}
|
|
42896
42801
|
/**
|
|
42897
|
-
* Gate for the clean best-shot native `keyFrame` (design 2026-07-21): the keyFrame
|
|
42898
|
-
* is the track's CLEAN native best frame and MUST NEVER be persisted from the
|
|
42899
|
-
* ≤640 RAM fallback (`tier: 'ram-fullframe'`) — that would pin the stored keyFrame
|
|
42900
|
-
* at detection resolution. On a `ram-fullframe` tier the caller drops the write
|
|
42901
|
-
* and the per-frame re-fire retries a real native surface later.
|
|
42902
|
-
*
|
|
42903
|
-
* An ABSENT tier (`undefined`, from a pre-tier runner on a mixed-version cluster)
|
|
42904
|
-
* is ACCEPTED exactly as before the tier field existed — back-compat, never break
|
|
42905
|
-
* mixed-version clusters. Only an explicit `ram-fullframe` is rejected.
|
|
42906
|
-
*
|
|
42907
|
-
* NOTE: this gates the native `keyFrame` ONLY. The 960 boxed TIMELINE tiles keep
|
|
42908
|
-
* accepting the ≤640 fallback (an honest low-res timeline is acceptable there).
|
|
42909
|
-
*/
|
|
42910
|
-
function keyFrameAcceptsTier(tier) {
|
|
42911
|
-
return tier !== "ram-fullframe";
|
|
42912
|
-
}
|
|
42913
|
-
/**
|
|
42914
|
-
* Encode BOTH key-frame variants from a single decoded native full frame. No box
|
|
42915
|
-
* is drawn (the `keyFrame` is the clean best full frame); `drawBoxedFrame([])`
|
|
42916
|
-
* is used purely as the raw-rgb → JPEG (+ optional downscale) encoder so the two
|
|
42917
|
-
* variants share one code path. The caller has ALREADY fetched the native frame
|
|
42918
|
-
* at {@link KEYFRAME_NATIVE_MAX_WIDTH}, so `keyFrame` re-encodes it verbatim
|
|
42919
|
-
* (no resize) and `keyFrameSmall` downscales the same buffer to ≤960 —
|
|
42920
|
-
* `withoutEnlargement` never upscales a sub-960 native frame.
|
|
42921
|
-
*/
|
|
42922
|
-
async function encodeKeyFrameVariants(native) {
|
|
42923
|
-
return {
|
|
42924
|
-
keyFrame: await drawBoxedFrame(native.data, native.width, native.height, [], { quality: 88 }),
|
|
42925
|
-
keyFrameSmall: await drawBoxedFrame(native.data, native.width, native.height, [], {
|
|
42926
|
-
quality: 88,
|
|
42927
|
-
maxWidth: 960
|
|
42928
|
-
})
|
|
42929
|
-
};
|
|
42930
|
-
}
|
|
42931
|
-
/**
|
|
42932
42802
|
* The web companion from the PARKED key frame's own JPEG (D422): the parcel is
|
|
42933
42803
|
* stored verbatim as `keyFrame`, and `keyFrameSmall` is this — the same bytes
|
|
42934
42804
|
* downscaled to {@link KEYFRAME_SMALL_MAX_WIDTH}. One decode, one encode,
|
|
42935
42805
|
* never a second fetch; `withoutEnlargement` never upscales a sub-960 frame.
|
|
42936
|
-
* The by-handle twin ({@link encodeKeyFrameVariants}) remains for the
|
|
42937
|
-
* summary group shot, which still fetches a native surface by handle.
|
|
42938
42806
|
*/
|
|
42939
42807
|
async function deriveKeyFrameSmall(keyFrameJpeg) {
|
|
42940
42808
|
return sharp(keyFrameJpeg).resize(960, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
|
|
@@ -43157,7 +43025,7 @@ async function mapWithConcurrency(items, limit, mapper) {
|
|
|
43157
43025
|
* all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
|
|
43158
43026
|
* all-zero track scores 0.
|
|
43159
43027
|
*/
|
|
43160
|
-
var WEIGHT_CONFIDENCE
|
|
43028
|
+
var WEIGHT_CONFIDENCE = .3;
|
|
43161
43029
|
var WEIGHT_CLASS = .2;
|
|
43162
43030
|
var WEIGHT_DWELL = .2;
|
|
43163
43031
|
var WEIGHT_PROXIMITY = .12;
|
|
@@ -43195,7 +43063,7 @@ var ANIMAL_CLASSES = new Set([
|
|
|
43195
43063
|
"cat"
|
|
43196
43064
|
]);
|
|
43197
43065
|
/** Clamp to [0,1]. */
|
|
43198
|
-
function clamp01$
|
|
43066
|
+
function clamp01$1(x) {
|
|
43199
43067
|
if (Number.isNaN(x)) return 0;
|
|
43200
43068
|
if (x < 0) return 0;
|
|
43201
43069
|
if (x > 1) return 1;
|
|
@@ -43229,7 +43097,7 @@ function computeImportance(input) {
|
|
|
43229
43097
|
const terms = [
|
|
43230
43098
|
{
|
|
43231
43099
|
reason: "confidence",
|
|
43232
|
-
value: WEIGHT_CONFIDENCE
|
|
43100
|
+
value: WEIGHT_CONFIDENCE * clamp01$1(input.peakConfidence)
|
|
43233
43101
|
},
|
|
43234
43102
|
{
|
|
43235
43103
|
reason: "class",
|
|
@@ -43237,15 +43105,15 @@ function computeImportance(input) {
|
|
|
43237
43105
|
},
|
|
43238
43106
|
{
|
|
43239
43107
|
reason: "dwell",
|
|
43240
|
-
value: WEIGHT_DWELL * clamp01$
|
|
43108
|
+
value: WEIGHT_DWELL * clamp01$1(input.durationMs / DWELL_FULL_MS)
|
|
43241
43109
|
},
|
|
43242
43110
|
{
|
|
43243
43111
|
reason: "proximity",
|
|
43244
|
-
value: WEIGHT_PROXIMITY * clamp01$
|
|
43112
|
+
value: WEIGHT_PROXIMITY * clamp01$1(input.peakBboxAreaFrac / SIZE_FULL)
|
|
43245
43113
|
},
|
|
43246
43114
|
{
|
|
43247
43115
|
reason: "travel",
|
|
43248
|
-
value: WEIGHT_TRAVEL * clamp01$
|
|
43116
|
+
value: WEIGHT_TRAVEL * clamp01$1(input.totalDistance / 2)
|
|
43249
43117
|
},
|
|
43250
43118
|
{
|
|
43251
43119
|
reason: "zone",
|
|
@@ -43264,11 +43132,11 @@ function computeImportance(input) {
|
|
|
43264
43132
|
}
|
|
43265
43133
|
const identified = input.identityLabel !== void 0 && input.identityLabel.length > 0;
|
|
43266
43134
|
if (input.netDisplacementFrac !== void 0 && input.pathSpanFrac !== void 0 && input.netDisplacementFrac < .02 && input.pathSpanFrac < .04 && !identified) return {
|
|
43267
|
-
importance: clamp01$
|
|
43135
|
+
importance: clamp01$1(sum * STATIC_IMPORTANCE_MULTIPLIER),
|
|
43268
43136
|
reason: best.reason
|
|
43269
43137
|
};
|
|
43270
43138
|
return {
|
|
43271
|
-
importance: clamp01$
|
|
43139
|
+
importance: clamp01$1(sum),
|
|
43272
43140
|
reason: best.reason
|
|
43273
43141
|
};
|
|
43274
43142
|
}
|
|
@@ -46258,7 +46126,7 @@ var ClosedRowMediaGate = class {
|
|
|
46258
46126
|
* it, so it can be loose enough to see the near misses.
|
|
46259
46127
|
*/
|
|
46260
46128
|
var CO_MOVING_MIN_CONTAINMENT = .6;
|
|
46261
|
-
function intersectionArea$
|
|
46129
|
+
function intersectionArea$3(a, b) {
|
|
46262
46130
|
return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
46263
46131
|
}
|
|
46264
46132
|
/**
|
|
@@ -46278,7 +46146,7 @@ function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMEN
|
|
|
46278
46146
|
if (o.className === b.className) continue;
|
|
46279
46147
|
const areaO = o.bbox.w * o.bbox.h;
|
|
46280
46148
|
if (areaO <= 0) continue;
|
|
46281
|
-
const inter = intersectionArea$
|
|
46149
|
+
const inter = intersectionArea$3(b.bbox, o.bbox);
|
|
46282
46150
|
if (inter <= 0) continue;
|
|
46283
46151
|
const union = areaB + areaO - inter;
|
|
46284
46152
|
const containment = inter / Math.min(areaB, areaO);
|
|
@@ -47028,10 +46896,127 @@ function pairRiders(detections) {
|
|
|
47028
46896
|
}
|
|
47029
46897
|
return pairs;
|
|
47030
46898
|
}
|
|
46899
|
+
/**
|
|
46900
|
+
* Floor on the predictive-drift clamp, in box sides.
|
|
46901
|
+
*
|
|
46902
|
+
* The scaled term collapses at short intervals — at the 56 ms minimum measured
|
|
46903
|
+
* on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
|
|
46904
|
+
* detector box jitter into a refused match. A quarter of a side is slack, not
|
|
46905
|
+
* travel.
|
|
46906
|
+
*/
|
|
46907
|
+
var DRIFT_MIN_SIDES = .25;
|
|
46908
|
+
/**
|
|
46909
|
+
* Re-acquisition reach, in box DIAGONALS per second.
|
|
46910
|
+
*
|
|
46911
|
+
* Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
|
|
46912
|
+
* centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
|
|
46913
|
+
* is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
|
|
46914
|
+
* physically bounded — 1.5 of a subject's own body-diagonal per second is a
|
|
46915
|
+
* brisk run, and being scale-free it means the same thing for a far-field
|
|
46916
|
+
* person with a 90-px box as for a near one with a 900-px box.
|
|
46917
|
+
*
|
|
46918
|
+
* The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
|
|
46919
|
+
* doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
|
|
46920
|
+
* a walking subject, and at a 150 ms burst interval it was loose enough to hand
|
|
46921
|
+
* one subject's id to another 452 px away (track `27855a56`, a second car, at
|
|
46922
|
+
* IoU 0.058). One number, two opposite failures.
|
|
46923
|
+
*/
|
|
46924
|
+
var REACH_DIAGONALS_PER_SEC = 1.5;
|
|
46925
|
+
/**
|
|
46926
|
+
* Floor on the re-acquisition reach, in box diagonals.
|
|
46927
|
+
*
|
|
46928
|
+
* Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
|
|
46929
|
+
* interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
|
|
46930
|
+
* is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
|
|
46931
|
+
* bought that track a second car. A short gap is evidence that the subject
|
|
46932
|
+
* cannot have gone far, and the gate now says so.
|
|
46933
|
+
*/
|
|
46934
|
+
var REACH_MIN_DIAGONALS = .3;
|
|
46935
|
+
/**
|
|
46936
|
+
* Ceiling on the re-acquisition reach, in box diagonals.
|
|
46937
|
+
*
|
|
46938
|
+
* `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
|
|
46939
|
+
* `maxMissedMs` gets 1.5 diagonals of reach, not 6.
|
|
46940
|
+
*/
|
|
46941
|
+
var REACH_MAX_DIAGONALS = 1.5;
|
|
46942
|
+
function clampRange(value, min, max) {
|
|
46943
|
+
return Math.max(min, Math.min(max, value));
|
|
46944
|
+
}
|
|
46945
|
+
/**
|
|
46946
|
+
* How far a coasting box's velocity extrapolation may carry it on one axis.
|
|
46947
|
+
*
|
|
46948
|
+
* `side` is the box's width for the x drift and its height for the y drift, so
|
|
46949
|
+
* the allowance keeps the same meaning for a tall person and a wide car.
|
|
46950
|
+
*/
|
|
46951
|
+
function driftAllowancePx(side, elapsedMs) {
|
|
46952
|
+
const elapsed = Math.max(0, elapsedMs);
|
|
46953
|
+
return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
|
|
46954
|
+
}
|
|
46955
|
+
/**
|
|
46956
|
+
* How far from where a track is EXPECTED a detection may sit and still be
|
|
46957
|
+
* claimed as that track by the loose (rescue / resurrection) reach clause.
|
|
46958
|
+
*
|
|
46959
|
+
* Scale-free: the budget is a multiple of the track's own diagonal, so it does
|
|
46960
|
+
* not have to know the frame size, the camera, or how far away the subject is.
|
|
46961
|
+
*/
|
|
46962
|
+
function reacquireReachPx(bbox, elapsedMs) {
|
|
46963
|
+
const diag = Math.hypot(bbox.w, bbox.h);
|
|
46964
|
+
const elapsed = Math.max(0, elapsedMs);
|
|
46965
|
+
return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
|
|
46966
|
+
}
|
|
46967
|
+
/**
|
|
46968
|
+
* Resurrection reach, in box DIAGONALS per second of gap (D665).
|
|
46969
|
+
*
|
|
46970
|
+
* A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
|
|
46971
|
+
* rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
|
|
46972
|
+
* nothing useful here. A subject the detector has not boxed for four seconds
|
|
46973
|
+
* is, on this fleet's evidence, one that stood still or stood half-hidden,
|
|
46974
|
+
* not one striding across the frame (a striding person is detected): 0.25
|
|
46975
|
+
* diagonals per second is a slow walk for a mid-field person and a pause for
|
|
46976
|
+
* a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
|
|
46977
|
+
* camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
|
|
46978
|
+
* i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
|
|
46979
|
+
*/
|
|
46980
|
+
var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
|
|
46981
|
+
/** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
|
|
46982
|
+
var RESURRECTION_REACH_MIN_DIAGONALS = .75;
|
|
46983
|
+
/**
|
|
46984
|
+
* How far from a corpse's last box a detection may sit and still resurrect it
|
|
46985
|
+
* on the proximity clause, for a corpse last seen `gapMs` ago.
|
|
46986
|
+
*/
|
|
46987
|
+
function resurrectionReachPx(bbox, gapMs) {
|
|
46988
|
+
const diag = Math.hypot(bbox.w, bbox.h);
|
|
46989
|
+
return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
|
|
46990
|
+
}
|
|
46991
|
+
/**
|
|
46992
|
+
* The classes D665's two relaxations are for: the low-score continuation tier
|
|
46993
|
+
* and the time-scaled resurrection gate. Defined ONCE, here, and read by both
|
|
46994
|
+
* (`low-score-continuation.ts`, `SortTracker.resurrectionTimeScaledFor`),
|
|
46995
|
+
* because the argument for each is the same argument about the same subjects
|
|
46996
|
+
* (D665, D678).
|
|
46997
|
+
*
|
|
46998
|
+
* D665 grew the graveyard reach with the gap on the argument that a subject
|
|
46999
|
+
* nobody has boxed for more than 4 s stood still or stood half-hidden — true
|
|
47000
|
+
* of a person and of an animal, and what the refused returns on camera 590
|
|
47001
|
+
* were. A car does not stand half-hidden for ten seconds and then reappear
|
|
47002
|
+
* one and a half diagonals away: the next car arrives. Camera 617,
|
|
47003
|
+
* 2026-09-28 08:47:32Z: a car at the gate inherited the id of the car that
|
|
47004
|
+
* had left the lot 9.85 s earlier, 1.51 diagonals away. A `vehicle` corpse
|
|
47005
|
+
* keeps the flat `rescueCentroidFactor` reach and the plausible-birth veto
|
|
47006
|
+
* it had before D665 — the same exclusion D665 made for the low-score tier,
|
|
47007
|
+
* where a sub-floor box on a parked car beside a passing one is the D410 hop.
|
|
47008
|
+
*/
|
|
47009
|
+
var LINGERING_SUBJECT_CLASSES = new Set(["person", "animal"]);
|
|
47010
|
+
/** Larger area / smaller area; `Infinity` when either box is empty. */
|
|
47011
|
+
function boxAreaRatio(a, b) {
|
|
47012
|
+
const areaA = a.w * a.h;
|
|
47013
|
+
const areaB = b.w * b.h;
|
|
47014
|
+
const smaller = Math.min(areaA, areaB);
|
|
47015
|
+
return smaller > 0 ? Math.max(areaA, areaB) / smaller : Number.POSITIVE_INFINITY;
|
|
47016
|
+
}
|
|
47031
47017
|
//#endregion
|
|
47032
47018
|
//#region src/pipeline-analytics/pipeline/tracker/low-score-continuation.ts
|
|
47033
47019
|
var LOW_SCORE_CONTINUATION_FLOOR_DEFAULT = .35;
|
|
47034
|
-
var LOW_SCORE_CONTINUATION_CLASSES = new Set(["person", "animal"]);
|
|
47035
47020
|
/** `[x1, y1, x2, y2]` (the discard trail's tuple) → the tracker's rectangle. */
|
|
47036
47021
|
function toRect(bbox) {
|
|
47037
47022
|
const [x1, y1, x2, y2] = bbox;
|
|
@@ -47052,7 +47037,7 @@ function selectLowScoreContinuations(discarded, floor) {
|
|
|
47052
47037
|
for (const d of discarded) {
|
|
47053
47038
|
if (d.reason !== "below-threshold") continue;
|
|
47054
47039
|
if (d.score < floor) continue;
|
|
47055
|
-
if (!
|
|
47040
|
+
if (!LINGERING_SUBJECT_CLASSES.has(d.macroClass)) continue;
|
|
47056
47041
|
const bbox = toRect(d.bbox);
|
|
47057
47042
|
if (bbox.w <= 0 || bbox.h <= 0) continue;
|
|
47058
47043
|
out.push({
|
|
@@ -47133,10 +47118,12 @@ function newTrackId() {
|
|
|
47133
47118
|
* the counts are per camera and per detection source. */
|
|
47134
47119
|
var LooseBindSampler = class {
|
|
47135
47120
|
counts = /* @__PURE__ */ new Map();
|
|
47136
|
-
/** Increment the (stage, gate) counter and answer with the running
|
|
47137
|
-
* this decision should be reported, or `undefined` when it is
|
|
47138
|
-
|
|
47139
|
-
|
|
47121
|
+
/** Increment the (stage, gate, refusal) counter and answer with the running
|
|
47122
|
+
* total when this decision should be reported, or `undefined` when it is
|
|
47123
|
+
* sampled out. The refusal is part of the key so a rare refusal is not
|
|
47124
|
+
* sampled away behind a frequent one on the same gate. */
|
|
47125
|
+
next(stage, gate, refusal) {
|
|
47126
|
+
const key = `${stage}|${gate}|${refusal ?? ""}`;
|
|
47140
47127
|
const count = (this.counts.get(key) ?? 0) + 1;
|
|
47141
47128
|
this.counts.set(key, count);
|
|
47142
47129
|
if (count === 1 || count % 50 === 0) return count;
|
|
@@ -47145,98 +47132,6 @@ var LooseBindSampler = class {
|
|
|
47145
47132
|
this.counts.clear();
|
|
47146
47133
|
}
|
|
47147
47134
|
};
|
|
47148
|
-
/**
|
|
47149
|
-
* Floor on the predictive-drift clamp, in box sides.
|
|
47150
|
-
*
|
|
47151
|
-
* The scaled term collapses at short intervals — at the 56 ms minimum measured
|
|
47152
|
-
* on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
|
|
47153
|
-
* detector box jitter into a refused match. A quarter of a side is slack, not
|
|
47154
|
-
* travel.
|
|
47155
|
-
*/
|
|
47156
|
-
var DRIFT_MIN_SIDES = .25;
|
|
47157
|
-
/**
|
|
47158
|
-
* Re-acquisition reach, in box DIAGONALS per second.
|
|
47159
|
-
*
|
|
47160
|
-
* Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
|
|
47161
|
-
* centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
|
|
47162
|
-
* is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
|
|
47163
|
-
* physically bounded — 1.5 of a subject's own body-diagonal per second is a
|
|
47164
|
-
* brisk run, and being scale-free it means the same thing for a far-field
|
|
47165
|
-
* person with a 90-px box as for a near one with a 900-px box.
|
|
47166
|
-
*
|
|
47167
|
-
* The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
|
|
47168
|
-
* doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
|
|
47169
|
-
* a walking subject, and at a 150 ms burst interval it was loose enough to hand
|
|
47170
|
-
* one subject's id to another 452 px away (track `27855a56`, a second car, at
|
|
47171
|
-
* IoU 0.058). One number, two opposite failures.
|
|
47172
|
-
*/
|
|
47173
|
-
var REACH_DIAGONALS_PER_SEC = 1.5;
|
|
47174
|
-
/**
|
|
47175
|
-
* Floor on the re-acquisition reach, in box diagonals.
|
|
47176
|
-
*
|
|
47177
|
-
* Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
|
|
47178
|
-
* interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
|
|
47179
|
-
* is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
|
|
47180
|
-
* bought that track a second car. A short gap is evidence that the subject
|
|
47181
|
-
* cannot have gone far, and the gate now says so.
|
|
47182
|
-
*/
|
|
47183
|
-
var REACH_MIN_DIAGONALS = .3;
|
|
47184
|
-
/**
|
|
47185
|
-
* Ceiling on the re-acquisition reach, in box diagonals.
|
|
47186
|
-
*
|
|
47187
|
-
* `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
|
|
47188
|
-
* `maxMissedMs` gets 1.5 diagonals of reach, not 6.
|
|
47189
|
-
*/
|
|
47190
|
-
var REACH_MAX_DIAGONALS = 1.5;
|
|
47191
|
-
function clampRange(value, min, max) {
|
|
47192
|
-
return Math.max(min, Math.min(max, value));
|
|
47193
|
-
}
|
|
47194
|
-
/**
|
|
47195
|
-
* How far a coasting box's velocity extrapolation may carry it on one axis.
|
|
47196
|
-
*
|
|
47197
|
-
* `side` is the box's width for the x drift and its height for the y drift, so
|
|
47198
|
-
* the allowance keeps the same meaning for a tall person and a wide car.
|
|
47199
|
-
*/
|
|
47200
|
-
function driftAllowancePx(side, elapsedMs) {
|
|
47201
|
-
const elapsed = Math.max(0, elapsedMs);
|
|
47202
|
-
return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
|
|
47203
|
-
}
|
|
47204
|
-
/**
|
|
47205
|
-
* How far from where a track is EXPECTED a detection may sit and still be
|
|
47206
|
-
* claimed as that track by the loose (rescue / resurrection) reach clause.
|
|
47207
|
-
*
|
|
47208
|
-
* Scale-free: the budget is a multiple of the track's own diagonal, so it does
|
|
47209
|
-
* not have to know the frame size, the camera, or how far away the subject is.
|
|
47210
|
-
*/
|
|
47211
|
-
function reacquireReachPx(bbox, elapsedMs) {
|
|
47212
|
-
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47213
|
-
const elapsed = Math.max(0, elapsedMs);
|
|
47214
|
-
return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
|
|
47215
|
-
}
|
|
47216
|
-
/**
|
|
47217
|
-
* Resurrection reach, in box DIAGONALS per second of gap (D665).
|
|
47218
|
-
*
|
|
47219
|
-
* A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
|
|
47220
|
-
* rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
|
|
47221
|
-
* nothing useful here. A subject the detector has not boxed for four seconds
|
|
47222
|
-
* is, on this fleet's evidence, one that stood still or stood half-hidden,
|
|
47223
|
-
* not one striding across the frame (a striding person is detected): 0.25
|
|
47224
|
-
* diagonals per second is a slow walk for a mid-field person and a pause for
|
|
47225
|
-
* a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
|
|
47226
|
-
* camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
|
|
47227
|
-
* i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
|
|
47228
|
-
*/
|
|
47229
|
-
var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
|
|
47230
|
-
/** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
|
|
47231
|
-
var RESURRECTION_REACH_MIN_DIAGONALS = .75;
|
|
47232
|
-
/**
|
|
47233
|
-
* How far from a corpse's last box a detection may sit and still resurrect it
|
|
47234
|
-
* on the proximity clause, for a corpse last seen `gapMs` ago.
|
|
47235
|
-
*/
|
|
47236
|
-
function resurrectionReachPx(bbox, gapMs) {
|
|
47237
|
-
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47238
|
-
return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
|
|
47239
|
-
}
|
|
47240
47135
|
//#endregion
|
|
47241
47136
|
//#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
|
|
47242
47137
|
/**
|
|
@@ -47251,7 +47146,7 @@ function resurrectionReachPx(bbox, gapMs) {
|
|
|
47251
47146
|
* diagonal = 2.085) with the same ~10% headroom the old 1.35 gave it.
|
|
47252
47147
|
*/
|
|
47253
47148
|
var GROWTH_CENTROID_FACTOR = 2.3;
|
|
47254
|
-
function intersectionArea$
|
|
47149
|
+
function intersectionArea$2(a, b) {
|
|
47255
47150
|
const x1 = Math.max(a.x, b.x);
|
|
47256
47151
|
const y1 = Math.max(a.y, b.y);
|
|
47257
47152
|
const x2 = Math.min(a.x + a.w, b.x + b.w);
|
|
@@ -47290,7 +47185,7 @@ function growthAdjacentMatch(track, det, ctx) {
|
|
|
47290
47185
|
const larger = Math.max(trackArea, detArea);
|
|
47291
47186
|
const smaller = Math.min(trackArea, detArea);
|
|
47292
47187
|
if (larger / smaller < 1.5) return false;
|
|
47293
|
-
if (intersectionArea$
|
|
47188
|
+
if (intersectionArea$2(track, det) / smaller >= .5) return false;
|
|
47294
47189
|
const tc = bboxCentroid(track);
|
|
47295
47190
|
const dc = bboxCentroid(det);
|
|
47296
47191
|
return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
|
|
@@ -47689,10 +47584,10 @@ var SortTracker = class SortTracker {
|
|
|
47689
47584
|
}
|
|
47690
47585
|
/** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
|
|
47691
47586
|
* Nothing is computed when no observer is wired. */
|
|
47692
|
-
reportBind(stage, gate, track, det, timestamp) {
|
|
47587
|
+
reportBind(stage, gate, track, det, timestamp, refusal) {
|
|
47693
47588
|
const observer = this.bindObserver;
|
|
47694
47589
|
if (!observer) return;
|
|
47695
|
-
const bindCount = this.bindSampler.next(stage, gate);
|
|
47590
|
+
const bindCount = this.bindSampler.next(stage, gate, refusal);
|
|
47696
47591
|
if (bindCount === void 0) return;
|
|
47697
47592
|
const tc = bboxCentroid(track.bbox);
|
|
47698
47593
|
const dc = bboxCentroid(det.bbox);
|
|
@@ -47726,6 +47621,7 @@ var SortTracker = class SortTracker {
|
|
|
47726
47621
|
frameHeight: this.frameHeightPx,
|
|
47727
47622
|
dtMs: timestamp - track.lastSeen,
|
|
47728
47623
|
trackAgeMs: timestamp - track.firstSeen,
|
|
47624
|
+
...refusal !== void 0 ? { refusal } : {},
|
|
47729
47625
|
bindCount,
|
|
47730
47626
|
sampledEvery: 50
|
|
47731
47627
|
});
|
|
@@ -48066,15 +47962,45 @@ var SortTracker = class SortTracker {
|
|
|
48066
47962
|
* was written for; not of the pass as it runs: on camera 590 (2026-09-27)
|
|
48067
47963
|
* 38% of all re-splits were PEOPLE returning after 4–12 s, further than the
|
|
48068
47964
|
* flat gate, with frames flowing the whole time. `resurrectionTimeScaled`
|
|
48069
|
-
* off restores the flat gate
|
|
47965
|
+
* off restores the flat gate — and a `vehicle` corpse always has it (D678,
|
|
47966
|
+
* {@link resurrectionTimeScaledFor}).
|
|
48070
47967
|
*/
|
|
48071
47968
|
reacquireReach(track, timestamp, pass) {
|
|
48072
47969
|
const diag = Math.hypot(track.bbox.w, track.bbox.h);
|
|
48073
|
-
if (pass === "resurrection") return this.
|
|
47970
|
+
if (pass === "resurrection") return this.resurrectionTimeScaledFor(track.class) ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
|
|
48074
47971
|
if (!this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
|
|
48075
47972
|
return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
|
|
48076
47973
|
}
|
|
48077
47974
|
/**
|
|
47975
|
+
* Whether D665's graveyard gate — the reach that grows with the gap, and the
|
|
47976
|
+
* plausible-birth veto lifted for a sole subject — applies to a corpse of
|
|
47977
|
+
* `cls`. People and animals only (D678): a car gone for ten seconds did not
|
|
47978
|
+
* stand half-hidden, the next one arrived.
|
|
47979
|
+
*/
|
|
47980
|
+
resurrectionTimeScaledFor(cls) {
|
|
47981
|
+
return this.config.resurrectionTimeScaled && LINGERING_SUBJECT_CLASSES.has(cls);
|
|
47982
|
+
}
|
|
47983
|
+
/**
|
|
47984
|
+
* The size check a PROXIMITY-only resurrection needs (D678), for a pair the
|
|
47985
|
+
* loose gate accepted on its `centroid` clause and the plausible-birth veto
|
|
47986
|
+
* let through.
|
|
47987
|
+
*
|
|
47988
|
+
* The `centroid` clause says where a box is and nothing about what it is;
|
|
47989
|
+
* a box more than {@link RESURRECTION_MAX_AREA_RATIO} larger or smaller than
|
|
47990
|
+
* the corpse's last box is refused on it, and the refusal is returned so the
|
|
47991
|
+
* caller can name it. The pair may still bind on `growth`, the partial-crop
|
|
47992
|
+
* rule whose whole subject IS a size change and which `looseMatch` would
|
|
47993
|
+
* have reached had the centroid clause not answered first. With the default
|
|
47994
|
+
* coast budget that never happens — `growth` refuses a pair more than
|
|
47995
|
+
* `GROWTH_MAX_GAP_MS` (1 s) apart and a corpse has been unseen for
|
|
47996
|
+
* `maxMissedMs` (4 s) — but `maxMissedMs` is a per-camera setting down to 0.
|
|
47997
|
+
*/
|
|
47998
|
+
proximitySizeVerdict(corpse, det, timestamp) {
|
|
47999
|
+
if (boxAreaRatio(corpse.bbox, det.bbox) <= 2.5) return "centroid";
|
|
48000
|
+
if (growthAdjacentMatch(corpse.bbox, det.bbox, this.growthContext(corpse, timestamp))) return "growth";
|
|
48001
|
+
return "size-incompatible";
|
|
48002
|
+
}
|
|
48003
|
+
/**
|
|
48078
48004
|
* Whether a REACH bind would be unambiguous: this coasting track is the only
|
|
48079
48005
|
* subject of its association group the scene holds, and `det` is the only box
|
|
48080
48006
|
* of that group on the frame.
|
|
@@ -48488,13 +48414,26 @@ var SortTracker = class SortTracker {
|
|
|
48488
48414
|
let refused;
|
|
48489
48415
|
let refusedGate = "iou";
|
|
48490
48416
|
let refusedScore = -1;
|
|
48417
|
+
let refusedBy = "crowd-guard";
|
|
48491
48418
|
for (const lost of this.lostTracks) {
|
|
48492
48419
|
if (!lost.resurrectable) continue;
|
|
48493
48420
|
if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
|
|
48494
|
-
const
|
|
48495
|
-
if (
|
|
48496
|
-
const
|
|
48497
|
-
if (
|
|
48421
|
+
const loose = this.looseMatch(lost, det, timestamp, "resurrection");
|
|
48422
|
+
if (loose === null) continue;
|
|
48423
|
+
const vetoApplies = loose === "centroid" && this.isPlausibleBirth(det, frameArea);
|
|
48424
|
+
if (vetoApplies && !(this.resurrectionTimeScaledFor(lost.class) && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
|
|
48425
|
+
const gate = loose === "centroid" ? this.proximitySizeVerdict(lost, det, timestamp) : loose;
|
|
48426
|
+
if (gate === "size-incompatible") {
|
|
48427
|
+
const score = iou$2(lost.bbox, det.bbox);
|
|
48428
|
+
if (score > refusedScore) {
|
|
48429
|
+
refused = lost;
|
|
48430
|
+
refusedGate = "centroid";
|
|
48431
|
+
refusedScore = score;
|
|
48432
|
+
refusedBy = "size-incompatible";
|
|
48433
|
+
}
|
|
48434
|
+
continue;
|
|
48435
|
+
}
|
|
48436
|
+
const vetoLifted = vetoApplies && gate === "centroid";
|
|
48498
48437
|
const score = iou$2(lost.bbox, det.bbox);
|
|
48499
48438
|
const peers = this.sameClassPeerCount(lost, surviving);
|
|
48500
48439
|
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
@@ -48502,6 +48441,7 @@ var SortTracker = class SortTracker {
|
|
|
48502
48441
|
refused = lost;
|
|
48503
48442
|
refusedGate = gate;
|
|
48504
48443
|
refusedScore = score;
|
|
48444
|
+
refusedBy = "crowd-guard";
|
|
48505
48445
|
}
|
|
48506
48446
|
continue;
|
|
48507
48447
|
}
|
|
@@ -48514,7 +48454,7 @@ var SortTracker = class SortTracker {
|
|
|
48514
48454
|
}
|
|
48515
48455
|
}
|
|
48516
48456
|
if (!best) {
|
|
48517
|
-
if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
|
|
48457
|
+
if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp, refusedBy);
|
|
48518
48458
|
continue;
|
|
48519
48459
|
}
|
|
48520
48460
|
resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : bestVetoLifted ? "sole-subject-veto-lifted" : "sole-subject");
|
|
@@ -50369,7 +50309,6 @@ var EMPTY_CASCADE_TALLY = {
|
|
|
50369
50309
|
faces: 0,
|
|
50370
50310
|
plates: 0,
|
|
50371
50311
|
embeddings: 0,
|
|
50372
|
-
summaries: 0,
|
|
50373
50312
|
bytesReclaimed: 0
|
|
50374
50313
|
};
|
|
50375
50314
|
/** Fold a leaf store's receipts into a running tally. */
|
|
@@ -50413,13 +50352,6 @@ function addLeafCounts(tally, counts) {
|
|
|
50413
50352
|
bytesReclaimed
|
|
50414
50353
|
};
|
|
50415
50354
|
break;
|
|
50416
|
-
case "summaries":
|
|
50417
|
-
next = {
|
|
50418
|
-
...next,
|
|
50419
|
-
summaries: next.summaries + c.rows,
|
|
50420
|
-
bytesReclaimed
|
|
50421
|
-
};
|
|
50422
|
-
break;
|
|
50423
50355
|
}
|
|
50424
50356
|
}
|
|
50425
50357
|
return next;
|
|
@@ -50433,7 +50365,6 @@ function mergeCascadeTallies(a, b) {
|
|
|
50433
50365
|
faces: a.faces + b.faces,
|
|
50434
50366
|
plates: a.plates + b.plates,
|
|
50435
50367
|
embeddings: a.embeddings + b.embeddings,
|
|
50436
|
-
summaries: a.summaries + b.summaries,
|
|
50437
50368
|
bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
|
|
50438
50369
|
};
|
|
50439
50370
|
}
|
|
@@ -50496,7 +50427,6 @@ function absentCascadeLegs(stores) {
|
|
|
50496
50427
|
if (!stores.faceStore) absent.push("faces");
|
|
50497
50428
|
if (!stores.plateStore) absent.push("plates");
|
|
50498
50429
|
if (!stores.objectEmbeddingStore) absent.push("embeddings");
|
|
50499
|
-
if (!stores.summaryStore) absent.push("summaries");
|
|
50500
50430
|
return absent;
|
|
50501
50431
|
}
|
|
50502
50432
|
function buildAnalyticsCascadeRegistry(stores) {
|
|
@@ -50504,7 +50434,6 @@ function buildAnalyticsCascadeRegistry(stores) {
|
|
|
50504
50434
|
if (stores.faceStore) registry.push(stores.faceStore);
|
|
50505
50435
|
if (stores.plateStore) registry.push(stores.plateStore);
|
|
50506
50436
|
if (stores.objectEmbeddingStore) registry.push(stores.objectEmbeddingStore);
|
|
50507
|
-
if (stores.summaryStore) registry.push(stores.summaryStore);
|
|
50508
50437
|
return registry;
|
|
50509
50438
|
}
|
|
50510
50439
|
/**
|
|
@@ -55025,10 +54954,6 @@ function trackColumnsFor(projection, needsPositions = false) {
|
|
|
55025
54954
|
* whatever order the engine felt like and a cursor that cannot resume.
|
|
55026
54955
|
*/
|
|
55027
54956
|
var RECENT_IDS_COLUMNS = ["lastSeen"];
|
|
55028
|
-
/** How many track ids one filter pass may collect per tier. Bounded for the
|
|
55029
|
-
* same reason every bulk read here is: an unbounded scan of this table is
|
|
55030
|
-
* what D447 measured holding the settings thread for seconds. */
|
|
55031
|
-
var TRACK_MATCH_SCAN_LIMIT = 2e4;
|
|
55032
54957
|
/** Spreadable `columns` for a store query — omitted entirely for `full`. */
|
|
55033
54958
|
function projectionArg(projection, needsPositions = false) {
|
|
55034
54959
|
const columns = trackColumnsFor(projection, needsPositions);
|
|
@@ -56145,42 +56070,6 @@ var TrackStore = class {
|
|
|
56145
56070
|
* current subject out from under it. The orphan audit's grace window would
|
|
56146
56071
|
* hide this for an hour and then stop hiding it.
|
|
56147
56072
|
*/
|
|
56148
|
-
/**
|
|
56149
|
-
* Track ids whose CLASS, sub-class or instance matches any of `terms`.
|
|
56150
|
-
*
|
|
56151
|
-
* The summaries' filter asks its question here rather than on a copy: a
|
|
56152
|
-
* label is a fact about the TRACK, it is decided while the track lives and
|
|
56153
|
-
* can still be re-decided afterwards (class voting, a late face match, an
|
|
56154
|
-
* operator enrolling a face and renaming that person). A copy stamped onto
|
|
56155
|
-
* the summary would answer with whatever was true at one instant and never
|
|
56156
|
-
* learn — which is the whole reason this lookup exists instead.
|
|
56157
|
-
*
|
|
56158
|
-
* Three queries because the filter compiler has no OR across fields, and one
|
|
56159
|
-
* term can be any of the three tiers: `animal` is a class, `dog` is a
|
|
56160
|
-
* sub-class, `Roberta` is an instance. Bounded, like every other bulk read
|
|
56161
|
-
* here.
|
|
56162
|
-
*/
|
|
56163
|
-
async trackIdsMatching(terms) {
|
|
56164
|
-
const wanted = terms.filter((t) => t.length > 0);
|
|
56165
|
-
if (wanted.length === 0) return [];
|
|
56166
|
-
const ids = /* @__PURE__ */ new Set();
|
|
56167
|
-
for (const field of [
|
|
56168
|
-
"className",
|
|
56169
|
-
"label",
|
|
56170
|
-
"subLabel"
|
|
56171
|
-
]) {
|
|
56172
|
-
const rows = await this.store.query.query({
|
|
56173
|
-
collection: TRACKS_COLLECTION,
|
|
56174
|
-
filter: {
|
|
56175
|
-
whereIn: { [field]: [...wanted] },
|
|
56176
|
-
limit: TRACK_MATCH_SCAN_LIMIT
|
|
56177
|
-
},
|
|
56178
|
-
columns: ["id"]
|
|
56179
|
-
});
|
|
56180
|
-
for (const row of rows) ids.add(row.id);
|
|
56181
|
-
}
|
|
56182
|
-
return [...ids];
|
|
56183
|
-
}
|
|
56184
56073
|
async existingTrackIds(trackIds) {
|
|
56185
56074
|
const PAGE = 500;
|
|
56186
56075
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -65068,7 +64957,7 @@ var RetrainAnnotationStore = class {
|
|
|
65068
64957
|
};
|
|
65069
64958
|
//#endregion
|
|
65070
64959
|
//#region src/pipeline-analytics/retrain/retrain-geometry.ts
|
|
65071
|
-
function clamp01
|
|
64960
|
+
function clamp01(v) {
|
|
65072
64961
|
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
65073
64962
|
}
|
|
65074
64963
|
/** Is this a usable rectangle at all? A zero-area box is not a subject. */
|
|
@@ -65144,10 +65033,10 @@ function deriveZoneCropRect(bbox, zone) {
|
|
|
65144
65033
|
function deriveSubjectCropRect(bbox, padding) {
|
|
65145
65034
|
if (!isUsableRect(bbox)) return null;
|
|
65146
65035
|
const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
|
|
65147
|
-
const cropX = clamp01
|
|
65148
|
-
const cropY = clamp01
|
|
65149
|
-
const cropRight = clamp01
|
|
65150
|
-
const cropBottom = clamp01
|
|
65036
|
+
const cropX = clamp01(bbox.x - bbox.w * pad);
|
|
65037
|
+
const cropY = clamp01(bbox.y - bbox.h * pad);
|
|
65038
|
+
const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
|
|
65039
|
+
const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
|
|
65151
65040
|
const cropW = cropRight - cropX;
|
|
65152
65041
|
const cropH = cropBottom - cropY;
|
|
65153
65042
|
if (!(cropW > 0) || !(cropH > 0)) return null;
|
|
@@ -68690,7 +68579,7 @@ async function persistTrackKeyFrame(deps, input) {
|
|
|
68690
68579
|
meta: {
|
|
68691
68580
|
...drop.meta,
|
|
68692
68581
|
nodeId: parcel.nodeId,
|
|
68693
|
-
error: errMsg
|
|
68582
|
+
error: errMsg(err)
|
|
68694
68583
|
}
|
|
68695
68584
|
});
|
|
68696
68585
|
}
|
|
@@ -68710,7 +68599,7 @@ async function persistTrackKeyFrame(deps, input) {
|
|
|
68710
68599
|
keyFrameMediaId
|
|
68711
68600
|
};
|
|
68712
68601
|
} catch (err) {
|
|
68713
|
-
const error = errMsg
|
|
68602
|
+
const error = errMsg(err);
|
|
68714
68603
|
deps.logger.warn("key-frame write failed", {
|
|
68715
68604
|
...drop,
|
|
68716
68605
|
meta: {
|
|
@@ -69724,7 +69613,7 @@ function isOversizeBox(bbox, frameWidth, frameHeight) {
|
|
|
69724
69613
|
const frame = frameWidth * frameHeight;
|
|
69725
69614
|
return frame > 0 && bbox.w * bbox.h / frame >= .3;
|
|
69726
69615
|
}
|
|
69727
|
-
function intersectionArea$
|
|
69616
|
+
function intersectionArea$1(a, b) {
|
|
69728
69617
|
const ix1 = Math.max(a.x, b.x);
|
|
69729
69618
|
const iy1 = Math.max(a.y, b.y);
|
|
69730
69619
|
const ix2 = Math.min(a.x + a.w, b.x + b.w);
|
|
@@ -69737,7 +69626,7 @@ function detectionSwallowsEntry(det, parked) {
|
|
|
69737
69626
|
const detArea = det.w * det.h;
|
|
69738
69627
|
if (entryArea <= 0 || detArea < entryArea * 1.35) return false;
|
|
69739
69628
|
if (!centroidInside(det, centroid(parked))) return false;
|
|
69740
|
-
return intersectionArea$
|
|
69629
|
+
return intersectionArea$1(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
|
|
69741
69630
|
}
|
|
69742
69631
|
/**
|
|
69743
69632
|
* True when `det` is a FRAGMENT of the parked object — much smaller than the
|
|
@@ -69760,7 +69649,7 @@ function entrySwallowsDetection(parked, det) {
|
|
|
69760
69649
|
const detArea = det.w * det.h;
|
|
69761
69650
|
if (entryArea <= 0 || detArea <= 0) return false;
|
|
69762
69651
|
if (detArea > entryArea / 1.35) return false;
|
|
69763
|
-
return intersectionArea$
|
|
69652
|
+
return intersectionArea$1(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
|
|
69764
69653
|
}
|
|
69765
69654
|
function parkedEntryHoldsDetection(entry, detBbox, config) {
|
|
69766
69655
|
const overlap = iou(entry.bbox, detBbox);
|
|
@@ -70701,7 +70590,7 @@ var VEHICLE_CLASSES = [
|
|
|
70701
70590
|
"vehicle"
|
|
70702
70591
|
];
|
|
70703
70592
|
/** Area of the intersection of two boxes. `0` when they do not overlap. */
|
|
70704
|
-
function intersectionArea
|
|
70593
|
+
function intersectionArea(a, b) {
|
|
70705
70594
|
const left = Math.max(a.x, b.x);
|
|
70706
70595
|
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
70707
70596
|
const top = Math.max(a.y, b.y);
|
|
@@ -70718,7 +70607,7 @@ function intersectionArea$1(a, b) {
|
|
|
70718
70607
|
function containedFraction(person, vehicle) {
|
|
70719
70608
|
const area = person.w * person.h;
|
|
70720
70609
|
if (area <= 0) return 0;
|
|
70721
|
-
return intersectionArea
|
|
70610
|
+
return intersectionArea(person, vehicle) / area;
|
|
70722
70611
|
}
|
|
70723
70612
|
/**
|
|
70724
70613
|
* Is this person box contained in any vehicle box in the same frame?
|
|
@@ -74314,895 +74203,6 @@ async function reconcileGallerySamples(deps, options) {
|
|
|
74314
74203
|
stoppedByCeiling
|
|
74315
74204
|
};
|
|
74316
74205
|
}
|
|
74317
|
-
//#endregion
|
|
74318
|
-
//#region src/pipeline-analytics/summary/summary-membership.ts
|
|
74319
|
-
/**
|
|
74320
|
-
* Who belongs to a summary, and which member is its primary.
|
|
74321
|
-
*
|
|
74322
|
-
* ## What this corrects
|
|
74323
|
-
*
|
|
74324
|
-
* `openOrAbsorb` absorbed ANY track born while a summary was open on the
|
|
74325
|
-
* camera, and `primary` went to whoever opened it. Both were measured wrong on
|
|
74326
|
-
* the live hub on 2026-09-13: synthetic sensor tracks (`pa-synth-…`) sat inside
|
|
74327
|
-
* summaries describing "who was here together", and a dog that walked in ahead
|
|
74328
|
-
* of its owner outranked the person behind it.
|
|
74329
|
-
*
|
|
74330
|
-
* The operator's rule: "persona animale veicolo la priorità. Gli eventi extra
|
|
74331
|
-
* sono sempre separati come eventi sintetici di sensori e pacchi."
|
|
74332
|
-
*
|
|
74333
|
-
* ## Why exclusion and priority live together
|
|
74334
|
-
*
|
|
74335
|
-
* They are the same question asked twice — "what is a subject in a group" —
|
|
74336
|
-
* and answering it in two files is how the two drift. A class that stops being
|
|
74337
|
-
* a member must not keep a rank, and a class that gains a rank must be a
|
|
74338
|
-
* member.
|
|
74339
|
-
*
|
|
74340
|
-
* Pure, so the surprising cases are pinned rather than discovered: the dog that
|
|
74341
|
-
* arrives first, the tie between two people, and the member whose class is not
|
|
74342
|
-
* known yet.
|
|
74343
|
-
*/
|
|
74344
|
-
/** The synthetic marker. `synthetic-track.ts` mints these and nothing parses it
|
|
74345
|
-
* except this — which is the one place it must be parsed. */
|
|
74346
|
-
var SYNTHETIC_TRACK_PREFIX = "pa-synth-";
|
|
74347
|
-
/**
|
|
74348
|
-
* Classes the operator keeps OUT of a group, by name.
|
|
74349
|
-
*
|
|
74350
|
-
* A delivered package is an event about a PLACE, not a member of something that
|
|
74351
|
-
* arrived together — the operator named it alongside the sensors.
|
|
74352
|
-
*/
|
|
74353
|
-
var NON_MEMBER_CLASSES = new Set(["package"]);
|
|
74354
|
-
/**
|
|
74355
|
-
* Is this track a member of a group at all?
|
|
74356
|
-
*
|
|
74357
|
-
* An unknown class is NOT a reason to refuse. The grouping is the point and the
|
|
74358
|
-
* label is decoration, and nothing revisits this decision — a member dropped
|
|
74359
|
-
* here is dropped for the life of the summary.
|
|
74360
|
-
*/
|
|
74361
|
-
function isSummaryEligible(trackId, className) {
|
|
74362
|
-
if (trackId.startsWith(SYNTHETIC_TRACK_PREFIX)) return false;
|
|
74363
|
-
return className === null || className === void 0 || !NON_MEMBER_CLASSES.has(className);
|
|
74364
|
-
}
|
|
74365
|
-
/**
|
|
74366
|
-
* The operator's order: person › animal › vehicle › everything else.
|
|
74367
|
-
*
|
|
74368
|
-
* Higher is more important. Everything unnamed — including a class that has not
|
|
74369
|
-
* resolved yet — sits below all three rather than above them: promoting an
|
|
74370
|
-
* unknown would let a momentary mis-read take the group's identity, and the
|
|
74371
|
-
* group shot with it.
|
|
74372
|
-
*/
|
|
74373
|
-
var CLASS_RANK = new Map([
|
|
74374
|
-
["person", 3],
|
|
74375
|
-
["animal", 2],
|
|
74376
|
-
["vehicle", 1]
|
|
74377
|
-
]);
|
|
74378
|
-
function classPriority(className) {
|
|
74379
|
-
if (className === null || className === void 0) return 0;
|
|
74380
|
-
return CLASS_RANK.get(className) ?? 0;
|
|
74381
|
-
}
|
|
74382
|
-
/**
|
|
74383
|
-
* Which member is the primary: highest class rank, ties broken by EARLIEST
|
|
74384
|
-
* arrival.
|
|
74385
|
-
*
|
|
74386
|
-
* The tie-break matters more than it looks. Two people are equal, so without it
|
|
74387
|
-
* the primary would flip on every join between equals — and the primary is what
|
|
74388
|
-
* a consumer reads to name the group, so it would rename itself for no reason
|
|
74389
|
-
* an operator could see.
|
|
74390
|
-
*
|
|
74391
|
-
* `null` for no members, never a fabricated id.
|
|
74392
|
-
*/
|
|
74393
|
-
function primaryTrackId(members) {
|
|
74394
|
-
let best = null;
|
|
74395
|
-
for (const m of members) {
|
|
74396
|
-
if (best === null) {
|
|
74397
|
-
best = m;
|
|
74398
|
-
continue;
|
|
74399
|
-
}
|
|
74400
|
-
const rank = classPriority(m.className);
|
|
74401
|
-
const bestRank = classPriority(best.className);
|
|
74402
|
-
if (rank > bestRank || rank === bestRank && m.joinedAt < best.joinedAt) best = m;
|
|
74403
|
-
}
|
|
74404
|
-
return best?.trackId ?? null;
|
|
74405
|
-
}
|
|
74406
|
-
//#endregion
|
|
74407
|
-
//#region src/pipeline-analytics/summary/summary-store.ts
|
|
74408
|
-
/**
|
|
74409
|
-
* @durable class=ledger owner=pipeline-analytics
|
|
74410
|
-
* write="one row per SUMMARY — a window of co-present tracks on one camera
|
|
74411
|
-
* group. Opened by `openOrAbsorb` when the first member arrives, updated
|
|
74412
|
-
* in place while the window absorbs more, and finalised by `seal` when the
|
|
74413
|
-
* window closes. Per WINDOW, never per frame or per track: the sealed
|
|
74414
|
-
* summary is what a notification rule reads."
|
|
74415
|
-
* retention="age-keyed on `sealedAt`, swept with the rest of the analytics
|
|
74416
|
-
* retention. An unsealed row belongs to a window still open and is never
|
|
74417
|
-
* swept by age — it is closed or cascaded away with its members."
|
|
74418
|
-
*/
|
|
74419
|
-
var SUMMARIES_COLLECTION = "pipeline-analytics:summaries";
|
|
74420
|
-
/**
|
|
74421
|
-
* @durable class=ledger owner=pipeline-analytics
|
|
74422
|
-
* write="one row per (summary, track) membership, written as a window
|
|
74423
|
-
* absorbs a track. The join that lets a sealed summary name its subjects
|
|
74424
|
-
* without re-reading the track store."
|
|
74425
|
-
* retention="cascaded — a member dies with its summary (`delete-track-cascade`
|
|
74426
|
-
* counts them as leaves) or with the track it names. It carries no clock of
|
|
74427
|
-
* its own: an age sweep here could outlive or predecease the summary the
|
|
74428
|
-
* row only means anything inside of."
|
|
74429
|
-
*/
|
|
74430
|
-
var SUMMARY_MEMBERS_COLLECTION$1 = "pipeline-analytics:summary-members";
|
|
74431
|
-
var SUMMARIES_COLUMNS = [
|
|
74432
|
-
{
|
|
74433
|
-
name: "id",
|
|
74434
|
-
type: "TEXT",
|
|
74435
|
-
primaryKey: true,
|
|
74436
|
-
notNull: true
|
|
74437
|
-
},
|
|
74438
|
-
{
|
|
74439
|
-
name: "deviceId",
|
|
74440
|
-
type: "INTEGER",
|
|
74441
|
-
notNull: true
|
|
74442
|
-
},
|
|
74443
|
-
{
|
|
74444
|
-
name: "openedAt",
|
|
74445
|
-
type: "INTEGER",
|
|
74446
|
-
notNull: true
|
|
74447
|
-
},
|
|
74448
|
-
{
|
|
74449
|
-
name: "lastActiveAt",
|
|
74450
|
-
type: "INTEGER",
|
|
74451
|
-
notNull: true
|
|
74452
|
-
},
|
|
74453
|
-
{
|
|
74454
|
-
name: "sealedAt",
|
|
74455
|
-
type: "INTEGER"
|
|
74456
|
-
},
|
|
74457
|
-
(
|
|
74458
|
-
/** `deviceId` while open, NULL once sealed — the unique "one open per camera". */
|
|
74459
|
-
{
|
|
74460
|
-
name: "openDeviceId",
|
|
74461
|
-
type: "INTEGER"
|
|
74462
|
-
}),
|
|
74463
|
-
{
|
|
74464
|
-
name: "memberCount",
|
|
74465
|
-
type: "INTEGER",
|
|
74466
|
-
notNull: true
|
|
74467
|
-
}
|
|
74468
|
-
];
|
|
74469
|
-
var SUMMARIES_INDEXES = [(
|
|
74470
|
-
/**
|
|
74471
|
-
* NOT unique since 2026-09-13. A summary is a GROUP, not a camera session, so
|
|
74472
|
-
* one camera can hold several open at once — a family at the door while
|
|
74473
|
-
* somebody else crosses the far end of the drive, or a group that split and
|
|
74474
|
-
* whose halves are now two groups.
|
|
74475
|
-
*
|
|
74476
|
-
* The uniqueness was D359's third guarantee, and the other two survive it in
|
|
74477
|
-
* a stricter form: an open is still idempotent, now keyed on the BIRTH
|
|
74478
|
-
* (`deviceId`, `openedAt`, founding track) rather than on the camera and the
|
|
74479
|
-
* millisecond — two subjects detected in one frame are two groups, not one —
|
|
74480
|
-
* and sealing is still a conditional transition, addressed by id because
|
|
74481
|
-
* `(openDeviceId, openedAt)` no longer names one row. Those are what stopped
|
|
74482
|
-
* the 2026-08-24 incident of eleven sealed rows for one group; "one open per
|
|
74483
|
-
* camera" was the part that also forbade the model the operator asked for.
|
|
74484
|
-
*
|
|
74485
|
-
* The index itself is create-only in this system — `declareCollection` emits
|
|
74486
|
-
* `CREATE ... IF NOT EXISTS` and nothing anywhere drops an index — so on a
|
|
74487
|
-
* database that already has the UNIQUE version this declaration changes
|
|
74488
|
-
* nothing until that index is dropped by hand. Until then `openOrAbsorb`
|
|
74489
|
-
* degrades to exactly the old behaviour (the unique violation is caught and
|
|
74490
|
-
* the newcomer joins the winner), which is why the drop is a switch and not a
|
|
74491
|
-
* prerequisite.
|
|
74492
|
-
*/
|
|
74493
|
-
{
|
|
74494
|
-
name: "summaries_open_device",
|
|
74495
|
-
columns: ["openDeviceId"]
|
|
74496
|
-
}), {
|
|
74497
|
-
name: "summaries_device_opened",
|
|
74498
|
-
columns: ["deviceId", "openedAt"]
|
|
74499
|
-
}];
|
|
74500
|
-
var SUMMARY_MEMBERS_COLUMNS = [
|
|
74501
|
-
{
|
|
74502
|
-
name: "id",
|
|
74503
|
-
type: "TEXT",
|
|
74504
|
-
primaryKey: true,
|
|
74505
|
-
notNull: true
|
|
74506
|
-
},
|
|
74507
|
-
{
|
|
74508
|
-
name: "summaryId",
|
|
74509
|
-
type: "TEXT",
|
|
74510
|
-
notNull: true
|
|
74511
|
-
},
|
|
74512
|
-
{
|
|
74513
|
-
name: "trackId",
|
|
74514
|
-
type: "TEXT",
|
|
74515
|
-
notNull: true
|
|
74516
|
-
},
|
|
74517
|
-
{
|
|
74518
|
-
name: "deviceId",
|
|
74519
|
-
type: "INTEGER",
|
|
74520
|
-
notNull: true
|
|
74521
|
-
},
|
|
74522
|
-
(
|
|
74523
|
-
/** When the track joined — named `timestamp` so the orphan audit's declared-owner scope reads it. */
|
|
74524
|
-
{
|
|
74525
|
-
name: "timestamp",
|
|
74526
|
-
type: "INTEGER",
|
|
74527
|
-
notNull: true
|
|
74528
|
-
}),
|
|
74529
|
-
(
|
|
74530
|
-
/**
|
|
74531
|
-
* What this member IS to the summary: see {@link SummaryMemberRole}.
|
|
74532
|
-
*
|
|
74533
|
-
* NULLABLE, and not because a role is optional. `declareCollection` is
|
|
74534
|
-
* additive — it ALTERs the column in — and SQLite refuses to add a `NOT NULL`
|
|
74535
|
-
* column with no default to a table that already has rows. A default would be
|
|
74536
|
-
* worse than null: it would stamp `primary` onto every member written before
|
|
74537
|
-
* roles existed, including the siblings, and nothing downstream could tell
|
|
74538
|
-
* the fabricated ones from the real ones.
|
|
74539
|
-
*/
|
|
74540
|
-
{
|
|
74541
|
-
name: "role",
|
|
74542
|
-
type: "TEXT"
|
|
74543
|
-
}),
|
|
74544
|
-
(
|
|
74545
|
-
/**
|
|
74546
|
-
* The member's subject class, so "which classes were present" is answerable
|
|
74547
|
-
* without joining `tracks`. Nullable for the same additive reason, AND
|
|
74548
|
-
* because a birth genuinely may not know a class yet — the membership is the
|
|
74549
|
-
* point, the label is decoration.
|
|
74550
|
-
*/
|
|
74551
|
-
{
|
|
74552
|
-
name: "className",
|
|
74553
|
-
type: "TEXT"
|
|
74554
|
-
})
|
|
74555
|
-
];
|
|
74556
|
-
var SUMMARY_MEMBERS_INDEXES = [
|
|
74557
|
-
{
|
|
74558
|
-
name: "summary_members_summary",
|
|
74559
|
-
columns: ["summaryId"]
|
|
74560
|
-
},
|
|
74561
|
-
{
|
|
74562
|
-
name: "summary_members_track",
|
|
74563
|
-
columns: ["trackId"]
|
|
74564
|
-
},
|
|
74565
|
-
(
|
|
74566
|
-
/** The filter's own index: "which groups hold an `animal`" walks members by
|
|
74567
|
-
* class, never summaries by hope. */
|
|
74568
|
-
{
|
|
74569
|
-
name: "summary_members_class",
|
|
74570
|
-
columns: ["className"]
|
|
74571
|
-
})
|
|
74572
|
-
];
|
|
74573
|
-
/**
|
|
74574
|
-
* A stored role, or `null`. An unrecognised value reads as `null` rather than
|
|
74575
|
-
* being passed through: a consumer switching on this must not receive a string
|
|
74576
|
-
* outside the union just because a row was hand-edited or written by a newer
|
|
74577
|
-
* release.
|
|
74578
|
-
*/
|
|
74579
|
-
function readRole(raw) {
|
|
74580
|
-
return raw === "primary" || raw === "sibling" || raw === "child" ? raw : null;
|
|
74581
|
-
}
|
|
74582
|
-
function readClassName(raw) {
|
|
74583
|
-
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
74584
|
-
}
|
|
74585
|
-
/**
|
|
74586
|
-
* The key of an open, derived from the birth that founded it.
|
|
74587
|
-
*
|
|
74588
|
-
* The founding TRACK is part of it, and that is the whole point: the key used
|
|
74589
|
-
* to be `(deviceId, openedAt)`, which made an open idempotent — a retry writes
|
|
74590
|
-
* the same row — but also made two subjects detected in the SAME frame one
|
|
74591
|
-
* group, because they are born in the same millisecond. That is the ordinary
|
|
74592
|
-
* case on a gate camera, not a race. With the track in the key a retry still
|
|
74593
|
-
* collapses (same birth, same track) and two births stay two groups.
|
|
74594
|
-
*/
|
|
74595
|
-
/** The widest a JS Date can name — the open end of a half-bounded window. */
|
|
74596
|
-
var TIME_MIN = -864e13;
|
|
74597
|
-
var TIME_MAX = 864e13;
|
|
74598
|
-
/**
|
|
74599
|
-
* How many rows one paged read may materialise before the merge.
|
|
74600
|
-
*
|
|
74601
|
-
* The page is cut AFTER ordering, so the read must be able to serve any cursor
|
|
74602
|
-
* position — but it must also be bounded, because an unbounded read of this
|
|
74603
|
-
* table on a busy hub is the shape D447 measured holding the settings thread
|
|
74604
|
-
* for seconds. Summaries are far fewer than tracks by construction (14 940
|
|
74605
|
-
* against 30 583 on this hub), so this ceiling is generous.
|
|
74606
|
-
*/
|
|
74607
|
-
var RECENT_SUMMARY_SCAN_LIMIT = 5e3;
|
|
74608
|
-
/** How many member rows one filter pass may read per tier. Bounded for the same
|
|
74609
|
-
* reason as the scan above, and generous: members are one row per subject. */
|
|
74610
|
-
var MEMBER_MATCH_SCAN_LIMIT = 2e4;
|
|
74611
|
-
function summaryId(deviceId, openedAt, foundingTrackId) {
|
|
74612
|
-
return `sum-${deviceId}-${openedAt}-${foundingTrackId}`;
|
|
74613
|
-
}
|
|
74614
|
-
function memberRowId(summaryIdValue, trackId) {
|
|
74615
|
-
return `${summaryIdValue}:${trackId}`;
|
|
74616
|
-
}
|
|
74617
|
-
function num$1(value, fallback = 0) {
|
|
74618
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
74619
|
-
}
|
|
74620
|
-
function toRow$1(id, data) {
|
|
74621
|
-
return {
|
|
74622
|
-
id,
|
|
74623
|
-
deviceId: num$1(data["deviceId"]),
|
|
74624
|
-
openedAt: num$1(data["openedAt"]),
|
|
74625
|
-
lastActiveAt: num$1(data["lastActiveAt"]),
|
|
74626
|
-
sealedAt: typeof data["sealedAt"] === "number" ? data["sealedAt"] : null,
|
|
74627
|
-
memberCount: num$1(data["memberCount"])
|
|
74628
|
-
};
|
|
74629
|
-
}
|
|
74630
|
-
function isUniqueViolation(err) {
|
|
74631
|
-
return err instanceof Error && /UNIQUE constraint failed/i.test(err.message);
|
|
74632
|
-
}
|
|
74633
|
-
var SummaryStore = class {
|
|
74634
|
-
store;
|
|
74635
|
-
logger;
|
|
74636
|
-
onEvict;
|
|
74637
|
-
trackIdsMatching;
|
|
74638
|
-
constructor(deps) {
|
|
74639
|
-
this.store = deps.store;
|
|
74640
|
-
this.logger = deps.logger;
|
|
74641
|
-
this.onEvict = deps.onEvict;
|
|
74642
|
-
this.trackIdsMatching = deps.trackIdsMatching;
|
|
74643
|
-
}
|
|
74644
|
-
static async declare(store) {
|
|
74645
|
-
await Promise.all([store.declareCollection.mutate({
|
|
74646
|
-
collection: SUMMARIES_COLLECTION,
|
|
74647
|
-
columns: [...SUMMARIES_COLUMNS],
|
|
74648
|
-
indexes: [...SUMMARIES_INDEXES]
|
|
74649
|
-
}), store.declareCollection.mutate({
|
|
74650
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74651
|
-
columns: [...SUMMARY_MEMBERS_COLUMNS],
|
|
74652
|
-
indexes: [...SUMMARY_MEMBERS_INDEXES]
|
|
74653
|
-
})]);
|
|
74654
|
-
}
|
|
74655
|
-
/** The camera's OPEN summary, from the row — `null` when none. */
|
|
74656
|
-
async findOpen(deviceId) {
|
|
74657
|
-
const first = (await this.store.query.query({
|
|
74658
|
-
collection: SUMMARIES_COLLECTION,
|
|
74659
|
-
filter: {
|
|
74660
|
-
where: { openDeviceId: deviceId },
|
|
74661
|
-
limit: 1
|
|
74662
|
-
}
|
|
74663
|
-
}))[0];
|
|
74664
|
-
return first === void 0 ? null : toRow$1(first.id, first.data);
|
|
74665
|
-
}
|
|
74666
|
-
/**
|
|
74667
|
-
* Every OPEN group, or just one camera's when `deviceId` is given.
|
|
74668
|
-
*
|
|
74669
|
-
* Plural per camera since 2026-09-13: a summary is a group, not a camera
|
|
74670
|
-
* session, so "the open summary of this camera" stopped being a question with
|
|
74671
|
-
* an answer. Ordered oldest-first, so a caller with no better rule takes the
|
|
74672
|
-
* one that has been there longest — which is what `openOrAbsorb` does, and is
|
|
74673
|
-
* byte-for-byte what shipped when only one could exist.
|
|
74674
|
-
*/
|
|
74675
|
-
async listOpen(deviceId) {
|
|
74676
|
-
return (await this.store.query.query({
|
|
74677
|
-
collection: SUMMARIES_COLLECTION,
|
|
74678
|
-
filter: {
|
|
74679
|
-
where: deviceId === void 0 ? { sealedAt: null } : { openDeviceId: deviceId },
|
|
74680
|
-
limit: 1e4
|
|
74681
|
-
}
|
|
74682
|
-
})).filter((r) => r.data["openDeviceId"] !== null).map((r) => toRow$1(r.id, r.data)).toSorted((a, b) => a.openedAt - b.openedAt);
|
|
74683
|
-
}
|
|
74684
|
-
async membersOf(summaryIdValue) {
|
|
74685
|
-
return (await this.store.query.query({
|
|
74686
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74687
|
-
filter: {
|
|
74688
|
-
where: { summaryId: summaryIdValue },
|
|
74689
|
-
limit: 1e4
|
|
74690
|
-
}
|
|
74691
|
-
})).map((r) => ({
|
|
74692
|
-
summaryId: summaryIdValue,
|
|
74693
|
-
trackId: String(r.data["trackId"] ?? ""),
|
|
74694
|
-
deviceId: num$1(r.data["deviceId"]),
|
|
74695
|
-
joinedAt: num$1(r.data["timestamp"]),
|
|
74696
|
-
role: readRole(r.data["role"]),
|
|
74697
|
-
className: readClassName(r.data["className"])
|
|
74698
|
-
}));
|
|
74699
|
-
}
|
|
74700
|
-
/**
|
|
74701
|
-
* Put a track into the camera's open summary, opening one when there is
|
|
74702
|
-
* none. Racing opens are settled by the store: the loser's insert hits the
|
|
74703
|
-
* unique `openDeviceId` and is re-read as an absorb.
|
|
74704
|
-
*/
|
|
74705
|
-
/**
|
|
74706
|
-
* Open a NEW group for this track.
|
|
74707
|
-
*
|
|
74708
|
-
* The unique-violation branch is kept even though `summaries_open_device` is
|
|
74709
|
-
* no longer UNIQUE: the id is `f(deviceId, openedAt)`, so two births in the
|
|
74710
|
-
* SAME millisecond still collide on the primary key, and the right answer
|
|
74711
|
-
* there is still "join the row that won" — it is the same group.
|
|
74712
|
-
*/
|
|
74713
|
-
async openNew(input) {
|
|
74714
|
-
const id = summaryId(input.deviceId, input.at, input.trackId);
|
|
74715
|
-
try {
|
|
74716
|
-
await this.store.set.mutate({
|
|
74717
|
-
collection: SUMMARIES_COLLECTION,
|
|
74718
|
-
key: id,
|
|
74719
|
-
value: {
|
|
74720
|
-
deviceId: input.deviceId,
|
|
74721
|
-
openedAt: input.at,
|
|
74722
|
-
lastActiveAt: input.at,
|
|
74723
|
-
sealedAt: null,
|
|
74724
|
-
openDeviceId: input.deviceId,
|
|
74725
|
-
memberCount: 1
|
|
74726
|
-
}
|
|
74727
|
-
});
|
|
74728
|
-
} catch (err) {
|
|
74729
|
-
if (!isUniqueViolation(err)) throw err;
|
|
74730
|
-
const winner = (await this.listOpen(input.deviceId)).find((s) => s.id === id);
|
|
74731
|
-
if (winner === void 0) throw err;
|
|
74732
|
-
return this.absorbInto(winner, input);
|
|
74733
|
-
}
|
|
74734
|
-
await this.addMember(id, input, "primary");
|
|
74735
|
-
return {
|
|
74736
|
-
id,
|
|
74737
|
-
deviceId: input.deviceId,
|
|
74738
|
-
openedAt: input.at,
|
|
74739
|
-
lastActiveAt: input.at,
|
|
74740
|
-
sealedAt: null,
|
|
74741
|
-
memberCount: 1
|
|
74742
|
-
};
|
|
74743
|
-
}
|
|
74744
|
-
/**
|
|
74745
|
-
* Join the camera's oldest open group, or open one.
|
|
74746
|
-
*
|
|
74747
|
-
* The default policy, for callers with no better rule. The rule that DOES
|
|
74748
|
-
* know better — proximity — lives with the caller that can see positions, and
|
|
74749
|
-
* reaches the store through {@link listOpen} + {@link absorbInto} instead.
|
|
74750
|
-
*/
|
|
74751
|
-
async openOrAbsorb(input) {
|
|
74752
|
-
const first = (await this.listOpen(input.deviceId))[0];
|
|
74753
|
-
return first === void 0 ? this.openNew(input) : this.absorbInto(first, input);
|
|
74754
|
-
}
|
|
74755
|
-
async absorbInto(summary, input) {
|
|
74756
|
-
const members = await this.membersOf(summary.id);
|
|
74757
|
-
if (members.some((m) => m.trackId === input.trackId)) return summary;
|
|
74758
|
-
const joined = {
|
|
74759
|
-
trackId: input.trackId,
|
|
74760
|
-
className: input.className ?? null,
|
|
74761
|
-
joinedAt: input.at
|
|
74762
|
-
};
|
|
74763
|
-
const primary = primaryTrackId([...members.map((m) => ({
|
|
74764
|
-
trackId: m.trackId,
|
|
74765
|
-
className: m.className,
|
|
74766
|
-
joinedAt: m.joinedAt
|
|
74767
|
-
})), joined]);
|
|
74768
|
-
await this.addMember(summary.id, input, primary === input.trackId ? "primary" : "sibling");
|
|
74769
|
-
if (primary === input.trackId) await this.demoteOtherPrimaries(summary.id, input.trackId, members);
|
|
74770
|
-
const memberCount = members.length + 1;
|
|
74771
|
-
const lastActiveAt = Math.max(summary.lastActiveAt, input.at);
|
|
74772
|
-
await this.store.updateWhere.mutate({
|
|
74773
|
-
collection: SUMMARIES_COLLECTION,
|
|
74774
|
-
filter: { where: {
|
|
74775
|
-
id: summary.id,
|
|
74776
|
-
openDeviceId: summary.deviceId
|
|
74777
|
-
} },
|
|
74778
|
-
data: {
|
|
74779
|
-
memberCount,
|
|
74780
|
-
lastActiveAt
|
|
74781
|
-
}
|
|
74782
|
-
});
|
|
74783
|
-
return {
|
|
74784
|
-
...summary,
|
|
74785
|
-
memberCount,
|
|
74786
|
-
lastActiveAt
|
|
74787
|
-
};
|
|
74788
|
-
}
|
|
74789
|
-
/**
|
|
74790
|
-
* Move a set of members out of an open group into a NEW open group.
|
|
74791
|
-
*
|
|
74792
|
-
* The two sides of a split. The departing members keep their join times, so
|
|
74793
|
-
* the group that leaves opens at the moment its earliest member joined the
|
|
74794
|
-
* old one — it existed then; what changed is that we can now tell.
|
|
74795
|
-
*
|
|
74796
|
-
* Order is deliberate: the new rows are written BEFORE the old ones are
|
|
74797
|
-
* dropped. A failure between the two leaves a member in both groups, which a
|
|
74798
|
-
* reader can see and a later pass can repair; the other order loses the
|
|
74799
|
-
* membership, and a lost member is the one wrong this store exists to
|
|
74800
|
-
* prevent (the same reasoning as {@link demoteOtherPrimaries}).
|
|
74801
|
-
*
|
|
74802
|
-
* `null` when there is nothing to do: no member matched, or ALL of them did.
|
|
74803
|
-
* Moving everybody is not a split — it would leave an empty group behind and
|
|
74804
|
-
* a new one holding exactly what the old one held.
|
|
74805
|
-
*/
|
|
74806
|
-
async splitOff(summary, departingTrackIds, at) {
|
|
74807
|
-
const members = await this.membersOf(summary.id);
|
|
74808
|
-
const leaving = members.filter((m) => departingTrackIds.includes(m.trackId));
|
|
74809
|
-
const staying = members.filter((m) => !departingTrackIds.includes(m.trackId));
|
|
74810
|
-
if (leaving.length === 0 || staying.length === 0) return null;
|
|
74811
|
-
const openedAt = Math.min(...leaving.map((m) => m.joinedAt));
|
|
74812
|
-
const founder = leaving.find((m) => m.joinedAt === openedAt) ?? leaving[0];
|
|
74813
|
-
const id = summaryId(summary.deviceId, openedAt, founder.trackId);
|
|
74814
|
-
await this.store.set.mutate({
|
|
74815
|
-
collection: SUMMARIES_COLLECTION,
|
|
74816
|
-
key: id,
|
|
74817
|
-
value: {
|
|
74818
|
-
deviceId: summary.deviceId,
|
|
74819
|
-
openedAt,
|
|
74820
|
-
lastActiveAt: summary.lastActiveAt,
|
|
74821
|
-
sealedAt: null,
|
|
74822
|
-
openDeviceId: summary.deviceId,
|
|
74823
|
-
memberCount: leaving.length
|
|
74824
|
-
}
|
|
74825
|
-
});
|
|
74826
|
-
const newPrimary = primaryTrackId(leaving);
|
|
74827
|
-
await this.store.insertMany.mutate({
|
|
74828
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74829
|
-
records: leaving.map((m) => ({
|
|
74830
|
-
id: memberRowId(id, m.trackId),
|
|
74831
|
-
data: {
|
|
74832
|
-
summaryId: id,
|
|
74833
|
-
trackId: m.trackId,
|
|
74834
|
-
deviceId: m.deviceId,
|
|
74835
|
-
timestamp: m.joinedAt,
|
|
74836
|
-
role: m.trackId === newPrimary ? "primary" : "sibling",
|
|
74837
|
-
className: m.className
|
|
74838
|
-
}
|
|
74839
|
-
}))
|
|
74840
|
-
});
|
|
74841
|
-
await this.store.deleteWhere.mutate({
|
|
74842
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74843
|
-
filter: {
|
|
74844
|
-
where: { summaryId: summary.id },
|
|
74845
|
-
whereIn: { trackId: leaving.map((m) => m.trackId) }
|
|
74846
|
-
}
|
|
74847
|
-
});
|
|
74848
|
-
const keptPrimary = primaryTrackId(staying);
|
|
74849
|
-
for (const m of staying) {
|
|
74850
|
-
const role = m.trackId === keptPrimary ? "primary" : "sibling";
|
|
74851
|
-
if (m.role === role) continue;
|
|
74852
|
-
await this.store.update.mutate({
|
|
74853
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74854
|
-
id: memberRowId(summary.id, m.trackId),
|
|
74855
|
-
data: { role }
|
|
74856
|
-
});
|
|
74857
|
-
}
|
|
74858
|
-
await this.store.updateWhere.mutate({
|
|
74859
|
-
collection: SUMMARIES_COLLECTION,
|
|
74860
|
-
filter: { where: {
|
|
74861
|
-
id: summary.id,
|
|
74862
|
-
openDeviceId: summary.deviceId
|
|
74863
|
-
} },
|
|
74864
|
-
data: { memberCount: staying.length }
|
|
74865
|
-
});
|
|
74866
|
-
this.logger.info("summary: a group walked apart", {
|
|
74867
|
-
tags: { deviceId: summary.deviceId },
|
|
74868
|
-
meta: {
|
|
74869
|
-
summaryId: summary.id,
|
|
74870
|
-
bornId: id,
|
|
74871
|
-
left: leaving.map((m) => m.trackId),
|
|
74872
|
-
stayed: staying.map((m) => m.trackId),
|
|
74873
|
-
at
|
|
74874
|
-
}
|
|
74875
|
-
});
|
|
74876
|
-
return {
|
|
74877
|
-
id,
|
|
74878
|
-
deviceId: summary.deviceId,
|
|
74879
|
-
openedAt,
|
|
74880
|
-
lastActiveAt: summary.lastActiveAt,
|
|
74881
|
-
sealedAt: null,
|
|
74882
|
-
memberCount: leaving.length
|
|
74883
|
-
};
|
|
74884
|
-
}
|
|
74885
|
-
/**
|
|
74886
|
-
* Put every OTHER member back to `sibling` after a new primary took the role.
|
|
74887
|
-
*
|
|
74888
|
-
* Best-effort per row and loud about a row it could not move: two primaries
|
|
74889
|
-
* is a wrong a consumer can see, but it is smaller than a lost member, so a
|
|
74890
|
-
* failure here never propagates to the join.
|
|
74891
|
-
*/
|
|
74892
|
-
async demoteOtherPrimaries(summaryIdValue, primaryTrack, previous) {
|
|
74893
|
-
for (const m of previous) {
|
|
74894
|
-
if (m.trackId === primaryTrack || m.role !== "primary") continue;
|
|
74895
|
-
try {
|
|
74896
|
-
await this.store.update.mutate({
|
|
74897
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74898
|
-
id: memberRowId(summaryIdValue, m.trackId),
|
|
74899
|
-
data: { role: "sibling" }
|
|
74900
|
-
});
|
|
74901
|
-
} catch (err) {
|
|
74902
|
-
this.logger.warn("summary: could not demote the previous primary — two remain", { meta: {
|
|
74903
|
-
summaryId: summaryIdValue,
|
|
74904
|
-
trackId: m.trackId,
|
|
74905
|
-
error: String(err)
|
|
74906
|
-
} });
|
|
74907
|
-
}
|
|
74908
|
-
}
|
|
74909
|
-
}
|
|
74910
|
-
async addMember(summaryIdValue, input, role) {
|
|
74911
|
-
await this.store.insertMany.mutate({
|
|
74912
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74913
|
-
records: [{
|
|
74914
|
-
id: memberRowId(summaryIdValue, input.trackId),
|
|
74915
|
-
data: {
|
|
74916
|
-
summaryId: summaryIdValue,
|
|
74917
|
-
trackId: input.trackId,
|
|
74918
|
-
deviceId: input.deviceId,
|
|
74919
|
-
timestamp: input.at,
|
|
74920
|
-
role,
|
|
74921
|
-
className: input.className ?? null
|
|
74922
|
-
}
|
|
74923
|
-
}]
|
|
74924
|
-
});
|
|
74925
|
-
}
|
|
74926
|
-
/**
|
|
74927
|
-
* Seal in place. `true` when THIS call sealed it; `false` when the row was
|
|
74928
|
-
* already sealed (or gone) — the second sealer learns, it does not write.
|
|
74929
|
-
*/
|
|
74930
|
-
async seal(summary, sealedAt) {
|
|
74931
|
-
const { updated } = await this.store.updateWhere.mutate({
|
|
74932
|
-
collection: SUMMARIES_COLLECTION,
|
|
74933
|
-
filter: { where: {
|
|
74934
|
-
id: summary.id,
|
|
74935
|
-
openDeviceId: summary.deviceId
|
|
74936
|
-
} },
|
|
74937
|
-
data: {
|
|
74938
|
-
sealedAt,
|
|
74939
|
-
openDeviceId: null
|
|
74940
|
-
}
|
|
74941
|
-
});
|
|
74942
|
-
if (updated > 1) this.logger.error("summary seal touched more than one row — the open key is not unique", {
|
|
74943
|
-
tags: { deviceId: summary.deviceId },
|
|
74944
|
-
meta: {
|
|
74945
|
-
summaryId: summary.id,
|
|
74946
|
-
updated
|
|
74947
|
-
}
|
|
74948
|
-
});
|
|
74949
|
-
return updated >= 1;
|
|
74950
|
-
}
|
|
74951
|
-
/**
|
|
74952
|
-
* Track cascade leaf: a member row whose track is gone is gone too, and
|
|
74953
|
-
* the summary's count follows. The envelope itself outlives its members
|
|
74954
|
-
* — it is an age-keyed root (`RETENTION_ROOTS`, `summary`).
|
|
74955
|
-
*/
|
|
74956
|
-
async deleteByTracks(trackIds) {
|
|
74957
|
-
if (trackIds.length === 0) return [{
|
|
74958
|
-
family: "summaries",
|
|
74959
|
-
rows: 0,
|
|
74960
|
-
bytes: 0
|
|
74961
|
-
}];
|
|
74962
|
-
const members = await this.store.query.query({
|
|
74963
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74964
|
-
filter: {
|
|
74965
|
-
whereIn: { trackId: [...trackIds] },
|
|
74966
|
-
limit: 1e4
|
|
74967
|
-
}
|
|
74968
|
-
});
|
|
74969
|
-
if (members.length === 0) return [{
|
|
74970
|
-
family: "summaries",
|
|
74971
|
-
rows: 0,
|
|
74972
|
-
bytes: 0
|
|
74973
|
-
}];
|
|
74974
|
-
const { deleted } = await this.store.deleteWhere.mutate({
|
|
74975
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
74976
|
-
filter: { whereIn: { trackId: [...trackIds] } }
|
|
74977
|
-
});
|
|
74978
|
-
const touched = new Set(members.map((m) => String(m.data["summaryId"] ?? "")));
|
|
74979
|
-
for (const summaryIdValue of touched) {
|
|
74980
|
-
const remaining = await this.membersOf(summaryIdValue);
|
|
74981
|
-
const raw = await this.store.get.query({
|
|
74982
|
-
collection: SUMMARIES_COLLECTION,
|
|
74983
|
-
key: summaryIdValue
|
|
74984
|
-
});
|
|
74985
|
-
if (typeof raw !== "object" || raw === null) continue;
|
|
74986
|
-
const row = raw;
|
|
74987
|
-
await this.store.updateWhere.mutate({
|
|
74988
|
-
collection: SUMMARIES_COLLECTION,
|
|
74989
|
-
filter: { where: {
|
|
74990
|
-
deviceId: num$1(row["deviceId"]),
|
|
74991
|
-
openedAt: num$1(row["openedAt"])
|
|
74992
|
-
} },
|
|
74993
|
-
data: { memberCount: remaining.length }
|
|
74994
|
-
});
|
|
74995
|
-
}
|
|
74996
|
-
return [{
|
|
74997
|
-
family: "summaries",
|
|
74998
|
-
rows: deleted,
|
|
74999
|
-
bytes: 0
|
|
75000
|
-
}];
|
|
75001
|
-
}
|
|
75002
|
-
/**
|
|
75003
|
-
* The surface's page (D359 phase 4): a camera set, newest first by
|
|
75004
|
-
* `openedAt`, inside an optional window, bounded. Ordered and bounded HERE
|
|
75005
|
-
* as well as in the query, so a store that ignores `orderBy` (the fake
|
|
75006
|
-
* does) cannot hand the surface an unordered page.
|
|
75007
|
-
*/
|
|
75008
|
-
async listRecent(input) {
|
|
75009
|
-
if (input.deviceIds.length === 0) return [];
|
|
75010
|
-
const matching = await this.summaryIdsMatching(input.classes);
|
|
75011
|
-
if (matching !== null && matching.size === 0) return [];
|
|
75012
|
-
const limit = Math.max(1, input.limit);
|
|
75013
|
-
return (await this.store.query.query({
|
|
75014
|
-
collection: SUMMARIES_COLLECTION,
|
|
75015
|
-
filter: {
|
|
75016
|
-
whereIn: { deviceId: [...input.deviceIds] },
|
|
75017
|
-
...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { openedAt: [input.since ?? 0, input.until ?? Number.MAX_SAFE_INTEGER] } } : {},
|
|
75018
|
-
orderBy: {
|
|
75019
|
-
field: "openedAt",
|
|
75020
|
-
direction: "desc"
|
|
75021
|
-
},
|
|
75022
|
-
limit
|
|
75023
|
-
}
|
|
75024
|
-
})).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);
|
|
75025
|
-
}
|
|
75026
|
-
/** One summary with its members, or `null`. */
|
|
75027
|
-
/**
|
|
75028
|
-
* One PAGE of summaries across a camera set, newest activity first.
|
|
75029
|
-
*
|
|
75030
|
-
* The same contract as `listRecentTracks` and for the same consumers: the
|
|
75031
|
-
* reel on the cameras page and the Events gallery both ride a cursor, so a
|
|
75032
|
-
* feed that could only answer "the last N" would make groups available in the
|
|
75033
|
-
* timeline and nowhere else. `recent-cursor.ts` is reused verbatim — the
|
|
75034
|
-
* ordering is the stable total order (`lastActiveAt` DESC, id DESC), and a
|
|
75035
|
-
* second implementation of that is a second place for two feeds to disagree
|
|
75036
|
-
* about what "newest" means.
|
|
75037
|
-
*
|
|
75038
|
-
* The window is on LAST ACTIVITY, not on the open: a session that started
|
|
75039
|
-
* before the window and ran into it belongs to the window, because it
|
|
75040
|
-
* happened there.
|
|
75041
|
-
*
|
|
75042
|
-
* Members for the WHOLE page come back in one read. One query per row is how
|
|
75043
|
-
* a page of two hundred becomes two hundred round trips — the shape that made
|
|
75044
|
-
* the old digest read two thousand tracks a camera.
|
|
75045
|
-
*/
|
|
75046
|
-
async queryRecent(input) {
|
|
75047
|
-
const deviceIds = [...new Set(input.deviceIds)];
|
|
75048
|
-
if (deviceIds.length === 0) return {
|
|
75049
|
-
summaries: [],
|
|
75050
|
-
nextCursor: null
|
|
75051
|
-
};
|
|
75052
|
-
const cursor = input.cursor === void 0 ? null : decodeRecentCursor(input.cursor);
|
|
75053
|
-
const matching = await this.summaryIdsMatching(input.classes);
|
|
75054
|
-
if (matching !== null && matching.size === 0) return {
|
|
75055
|
-
summaries: [],
|
|
75056
|
-
nextCursor: null
|
|
75057
|
-
};
|
|
75058
|
-
const ordered = (await this.store.query.query({
|
|
75059
|
-
collection: SUMMARIES_COLLECTION,
|
|
75060
|
-
filter: {
|
|
75061
|
-
whereIn: { deviceId: deviceIds },
|
|
75062
|
-
...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { lastActiveAt: [input.since ?? TIME_MIN, input.until ?? TIME_MAX] } } : {},
|
|
75063
|
-
limit: RECENT_SUMMARY_SCAN_LIMIT
|
|
75064
|
-
}
|
|
75065
|
-
})).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).map((row) => ({
|
|
75066
|
-
row,
|
|
75067
|
-
key: {
|
|
75068
|
-
lastSeen: row.lastActiveAt,
|
|
75069
|
-
id: row.id
|
|
75070
|
-
}
|
|
75071
|
-
})).filter((entry) => cursor === null || isAfterCursor(entry.key, cursor)).toSorted((a, b) => compareRecentDesc(a.key, b.key));
|
|
75072
|
-
const page = ordered.slice(0, input.limit);
|
|
75073
|
-
const last = page.at(-1);
|
|
75074
|
-
const nextCursor = last !== void 0 && ordered.length > page.length ? encodeRecentCursor({
|
|
75075
|
-
lastSeen: last.row.lastActiveAt,
|
|
75076
|
-
trackId: last.row.id
|
|
75077
|
-
}) : null;
|
|
75078
|
-
const members = await this.membersOfMany(page.map((e) => e.row.id));
|
|
75079
|
-
return {
|
|
75080
|
-
summaries: page.map((e) => ({
|
|
75081
|
-
summary: e.row,
|
|
75082
|
-
members: members.get(e.row.id) ?? []
|
|
75083
|
-
})),
|
|
75084
|
-
nextCursor
|
|
75085
|
-
};
|
|
75086
|
-
}
|
|
75087
|
-
/**
|
|
75088
|
-
* The summaries whose MEMBERS match any of `classes`, or `null` for no filter.
|
|
75089
|
-
*
|
|
75090
|
-
* TRACKS first, then the members that name them, then the summaries. The
|
|
75091
|
-
* label is a fact about the track and it is asked of the track — a summary
|
|
75092
|
-
* that stored its own copy would answer with whatever was true when it
|
|
75093
|
-
* sealed, and would never learn that a face was enrolled and a person named
|
|
75094
|
-
* an hour later. The operator's rule: *a summary keeps the minimum
|
|
75095
|
-
* information necessary*, and a duplicated label is not the minimum, it is a
|
|
75096
|
-
* second truth with an expiry date.
|
|
75097
|
-
*
|
|
75098
|
-
* Narrows before the page is cut. Filtering the other way round — a page of
|
|
75099
|
-
* summaries, then their members — returns a short or empty page while matches
|
|
75100
|
-
* sit further down the list, and the operator reads that as "there were none".
|
|
75101
|
-
*/
|
|
75102
|
-
async summaryIdsMatching(classes) {
|
|
75103
|
-
const wanted = (classes ?? []).filter((t) => t.length > 0);
|
|
75104
|
-
if (wanted.length === 0) return null;
|
|
75105
|
-
const matchTracks = this.trackIdsMatching;
|
|
75106
|
-
if (matchTracks === void 0) {
|
|
75107
|
-
this.logger.warn("summary filter: no track matcher wired — refusing to answer unfiltered", { meta: { classes: wanted.length } });
|
|
75108
|
-
return /* @__PURE__ */ new Set();
|
|
75109
|
-
}
|
|
75110
|
-
const trackIds = await matchTracks(wanted);
|
|
75111
|
-
if (trackIds.length === 0) return /* @__PURE__ */ new Set();
|
|
75112
|
-
const ids = /* @__PURE__ */ new Set();
|
|
75113
|
-
const PAGE = 500;
|
|
75114
|
-
for (let i = 0; i < trackIds.length; i += PAGE) {
|
|
75115
|
-
const chunk = trackIds.slice(i, i + PAGE);
|
|
75116
|
-
const rows = await this.store.query.query({
|
|
75117
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
75118
|
-
filter: {
|
|
75119
|
-
whereIn: { trackId: [...chunk] },
|
|
75120
|
-
limit: MEMBER_MATCH_SCAN_LIMIT
|
|
75121
|
-
}
|
|
75122
|
-
});
|
|
75123
|
-
for (const row of rows) ids.add(String(row.data["summaryId"] ?? ""));
|
|
75124
|
-
}
|
|
75125
|
-
ids.delete("");
|
|
75126
|
-
return ids;
|
|
75127
|
-
}
|
|
75128
|
-
/** Members of many summaries, in ONE read, grouped by summary id. */
|
|
75129
|
-
async membersOfMany(summaryIds) {
|
|
75130
|
-
const out = /* @__PURE__ */ new Map();
|
|
75131
|
-
if (summaryIds.length === 0) return out;
|
|
75132
|
-
const rows = await this.store.query.query({
|
|
75133
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
75134
|
-
filter: {
|
|
75135
|
-
whereIn: { summaryId: [...summaryIds] },
|
|
75136
|
-
limit: 1e4
|
|
75137
|
-
}
|
|
75138
|
-
});
|
|
75139
|
-
for (const r of rows) {
|
|
75140
|
-
const summaryIdValue = String(r.data["summaryId"] ?? "");
|
|
75141
|
-
const member = {
|
|
75142
|
-
summaryId: summaryIdValue,
|
|
75143
|
-
trackId: String(r.data["trackId"] ?? ""),
|
|
75144
|
-
deviceId: num$1(r.data["deviceId"]),
|
|
75145
|
-
joinedAt: num$1(r.data["timestamp"]),
|
|
75146
|
-
role: readRole(r.data["role"]),
|
|
75147
|
-
className: readClassName(r.data["className"])
|
|
75148
|
-
};
|
|
75149
|
-
const bucket = out.get(summaryIdValue);
|
|
75150
|
-
if (bucket === void 0) out.set(summaryIdValue, [member]);
|
|
75151
|
-
else bucket.push(member);
|
|
75152
|
-
}
|
|
75153
|
-
for (const bucket of out.values()) bucket.sort((a, b) => a.joinedAt - b.joinedAt);
|
|
75154
|
-
return out;
|
|
75155
|
-
}
|
|
75156
|
-
async get(id) {
|
|
75157
|
-
const raw = await this.store.get.query({
|
|
75158
|
-
collection: SUMMARIES_COLLECTION,
|
|
75159
|
-
key: id
|
|
75160
|
-
});
|
|
75161
|
-
if (typeof raw !== "object" || raw === null) return null;
|
|
75162
|
-
return {
|
|
75163
|
-
summary: toRow$1(id, raw),
|
|
75164
|
-
members: await this.membersOf(id)
|
|
75165
|
-
};
|
|
75166
|
-
}
|
|
75167
|
-
/** The subset of `ids` that still have a row — the orphan audit's probe. Throws on a failed read. */
|
|
75168
|
-
async existingIds(ids) {
|
|
75169
|
-
const found = /* @__PURE__ */ new Set();
|
|
75170
|
-
await Promise.all(ids.map(async (id) => {
|
|
75171
|
-
const raw = await this.store.get.query({
|
|
75172
|
-
collection: SUMMARIES_COLLECTION,
|
|
75173
|
-
key: id
|
|
75174
|
-
});
|
|
75175
|
-
if (typeof raw === "object" && raw !== null) found.add(id);
|
|
75176
|
-
}));
|
|
75177
|
-
return found;
|
|
75178
|
-
}
|
|
75179
|
-
/** Age sweep on the device window: everything opened before `cutoffMs`, members included. */
|
|
75180
|
-
async evictBefore(deviceId, cutoffMs) {
|
|
75181
|
-
const rows = await this.store.query.query({
|
|
75182
|
-
collection: SUMMARIES_COLLECTION,
|
|
75183
|
-
filter: {
|
|
75184
|
-
where: { deviceId },
|
|
75185
|
-
whereBetween: { openedAt: [0, cutoffMs - 1] },
|
|
75186
|
-
limit: 1e4
|
|
75187
|
-
}
|
|
75188
|
-
});
|
|
75189
|
-
if (rows.length === 0) return 0;
|
|
75190
|
-
const ids = rows.map((r) => r.id);
|
|
75191
|
-
await this.onEvict?.(ids);
|
|
75192
|
-
await this.store.deleteWhere.mutate({
|
|
75193
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
75194
|
-
filter: { whereIn: { summaryId: ids } }
|
|
75195
|
-
});
|
|
75196
|
-
const { deleted } = await this.store.deleteWhere.mutate({
|
|
75197
|
-
collection: SUMMARIES_COLLECTION,
|
|
75198
|
-
filter: {
|
|
75199
|
-
where: { deviceId },
|
|
75200
|
-
whereBetween: { openedAt: [0, cutoffMs - 1] }
|
|
75201
|
-
}
|
|
75202
|
-
});
|
|
75203
|
-
return deleted;
|
|
75204
|
-
}
|
|
75205
|
-
};
|
|
75206
74206
|
/**
|
|
75207
74207
|
* Where the completion marker lives.
|
|
75208
74208
|
*
|
|
@@ -78476,10 +77476,6 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
78476
77476
|
collection: PLATES_COLLECTION$1,
|
|
78477
77477
|
classification: derived(PLATE_COLUMNS)
|
|
78478
77478
|
},
|
|
78479
|
-
{
|
|
78480
|
-
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
78481
|
-
classification: derived(SUMMARY_MEMBERS_COLUMNS)
|
|
78482
|
-
},
|
|
78483
77479
|
{
|
|
78484
77480
|
collection: MEDIA_COLLECTION,
|
|
78485
77481
|
classification: {
|
|
@@ -78523,13 +77519,6 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
78523
77519
|
rootId: "ops-log"
|
|
78524
77520
|
}
|
|
78525
77521
|
},
|
|
78526
|
-
{
|
|
78527
|
-
collection: SUMMARIES_COLLECTION,
|
|
78528
|
-
classification: {
|
|
78529
|
-
kind: "retention-root",
|
|
78530
|
-
rootId: "summary"
|
|
78531
|
-
}
|
|
78532
|
-
},
|
|
78533
77522
|
{
|
|
78534
77523
|
collection: IDENTITIES_COLLECTION,
|
|
78535
77524
|
classification: {
|
|
@@ -78718,10 +77707,6 @@ function resolveMediaOwner(row) {
|
|
|
78718
77707
|
table: ownerKind,
|
|
78719
77708
|
eventId: ownerId
|
|
78720
77709
|
};
|
|
78721
|
-
case "summary": return {
|
|
78722
|
-
kind: "summary",
|
|
78723
|
-
summaryId: ownerId
|
|
78724
|
-
};
|
|
78725
77710
|
case "face": return ownerId.startsWith("face-") ? {
|
|
78726
77711
|
kind: "track",
|
|
78727
77712
|
trackId: ownerId.slice(FACE_MEDIA_OWNER_PREFIX.length)
|
|
@@ -78829,17 +77814,15 @@ var DEFAULT_ORPHAN_LIMITS = {
|
|
|
78829
77814
|
function questionsFor(refs) {
|
|
78830
77815
|
const trackIds = /* @__PURE__ */ new Set();
|
|
78831
77816
|
const eventIds = /* @__PURE__ */ new Map();
|
|
78832
|
-
const summaryIds = /* @__PURE__ */ new Set();
|
|
78833
77817
|
for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
|
|
78834
77818
|
else if (ref.kind === "event") {
|
|
78835
77819
|
const perTable = eventIds.get(ref.table) ?? /* @__PURE__ */ new Set();
|
|
78836
77820
|
perTable.add(ref.eventId);
|
|
78837
77821
|
eventIds.set(ref.table, perTable);
|
|
78838
|
-
}
|
|
77822
|
+
}
|
|
78839
77823
|
return {
|
|
78840
77824
|
trackIds: [...trackIds],
|
|
78841
|
-
eventIdsByTable: new Map([...eventIds].map(([table, ids]) => [table, [...ids]]))
|
|
78842
|
-
summaryIds: [...summaryIds]
|
|
77825
|
+
eventIdsByTable: new Map([...eventIds].map(([table, ids]) => [table, [...ids]]))
|
|
78843
77826
|
};
|
|
78844
77827
|
}
|
|
78845
77828
|
/**
|
|
@@ -78883,10 +77866,9 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
78883
77866
|
break;
|
|
78884
77867
|
}
|
|
78885
77868
|
const refs = rows.map((row) => scope.resolve(row));
|
|
78886
|
-
const { trackIds, eventIdsByTable
|
|
77869
|
+
const { trackIds, eventIdsByTable } = questionsFor(refs);
|
|
78887
77870
|
let liveTracks;
|
|
78888
77871
|
const liveEvents = /* @__PURE__ */ new Map();
|
|
78889
|
-
let liveSummaries;
|
|
78890
77872
|
let keptMedia = /* @__PURE__ */ new Set();
|
|
78891
77873
|
try {
|
|
78892
77874
|
liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
|
|
@@ -78895,7 +77877,6 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
78895
77877
|
if (ids.length === 0) continue;
|
|
78896
77878
|
liveEvents.set(table, await deps.liveEventIds(table, ids));
|
|
78897
77879
|
}
|
|
78898
|
-
liveSummaries = summaryIds.length > 0 ? await deps.liveSummaryIds(summaryIds) : /* @__PURE__ */ new Set();
|
|
78899
77880
|
} catch (err) {
|
|
78900
77881
|
deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
|
|
78901
77882
|
collection: scope.collection,
|
|
@@ -78923,7 +77904,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
78923
77904
|
tally.unresolvable += 1;
|
|
78924
77905
|
continue;
|
|
78925
77906
|
}
|
|
78926
|
-
if (ref.kind === "track" ? liveTracks.has(ref.trackId) || keptMedia.has(row.key) : ref.kind === "event" ? liveEvents.get(ref.table)?.has(ref.eventId) ?? false :
|
|
77907
|
+
if (ref.kind === "track" ? liveTracks.has(ref.trackId) || keptMedia.has(row.key) : ref.kind === "event" ? liveEvents.get(ref.table)?.has(ref.eventId) ?? false : false) continue;
|
|
78927
77908
|
const ts = num(row.data, scope.timestampField);
|
|
78928
77909
|
if (ts !== null && ts > graceFloor) {
|
|
78929
77910
|
tally.withinGrace += 1;
|
|
@@ -79093,11 +78074,7 @@ var ORPHAN_AUDIT_DEFAULTS = {
|
|
|
79093
78074
|
intervalHours: OrphanAuditIntervalHoursSchema.parse(void 0),
|
|
79094
78075
|
pageSize: OrphanAuditPageSizeSchema.parse(void 0)
|
|
79095
78076
|
};
|
|
79096
|
-
var RAW_DELETE_COLLECTIONS = new Set([
|
|
79097
|
-
"pipeline-analytics:object-events",
|
|
79098
|
-
"pipeline-analytics:plates",
|
|
79099
|
-
"pipeline-analytics:summary-members"
|
|
79100
|
-
]);
|
|
78077
|
+
var RAW_DELETE_COLLECTIONS = new Set(["pipeline-analytics:object-events", "pipeline-analytics:plates"]);
|
|
79101
78078
|
function buildOrphanReclaimer(ports) {
|
|
79102
78079
|
return async (collection, key) => {
|
|
79103
78080
|
if (collection === "pipeline-analytics:media") {
|
|
@@ -79238,14 +78215,12 @@ async function sweepTracklessRoots(deps) {
|
|
|
79238
78215
|
cutoffMs
|
|
79239
78216
|
});
|
|
79240
78217
|
const sensor = deps.evictSensorEvents ? await deps.evictSensorEvents(deviceId, cutoffMs) : 0;
|
|
79241
|
-
const summaries = deps.evictSummaries ? await deps.evictSummaries(deviceId, cutoffMs) : 0;
|
|
79242
78218
|
const evicted = {
|
|
79243
78219
|
motion: events.motion,
|
|
79244
78220
|
audio: events.audio,
|
|
79245
|
-
sensor
|
|
79246
|
-
summaries
|
|
78221
|
+
sensor
|
|
79247
78222
|
};
|
|
79248
|
-
if (evicted.motion.length + evicted.audio.length + evicted.sensor
|
|
78223
|
+
if (evicted.motion.length + evicted.audio.length + evicted.sensor > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
|
|
79249
78224
|
} catch (err) {
|
|
79250
78225
|
deps.onError(deviceId, err);
|
|
79251
78226
|
}
|
|
@@ -80022,11 +78997,12 @@ function isFullFrameBbox(bbox) {
|
|
|
80022
78997
|
/** Throttle for the native-crop HIT/FALLBACK metric window line. */
|
|
80023
78998
|
var NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
|
|
80024
78999
|
/**
|
|
80025
|
-
* Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (
|
|
80026
|
-
*
|
|
80027
|
-
*
|
|
80028
|
-
*
|
|
80029
|
-
*
|
|
79000
|
+
* Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (~1–1.5 MB
|
|
79001
|
+
* on a 4K camera) rather than a boxed timeline tile or a pinned ≤640 detection
|
|
79002
|
+
* frame (tens of kB). The two belong in different counters or the window line
|
|
79003
|
+
* says "full frames" and means two payload classes three orders of magnitude
|
|
79004
|
+
* apart. (D470) No caller asks for that rung since D422 (the key frame) and
|
|
79005
|
+
* D680 (the group shot); the counter stays so the next one is seen.
|
|
80030
79006
|
*/
|
|
80031
79007
|
var FULL_FRAME_TILE_WIDTH_CEILING = 1920;
|
|
80032
79008
|
function createNativeFrameTransport(deps) {
|
|
@@ -80103,8 +79079,8 @@ function createNativeFrameTransport(deps) {
|
|
|
80103
79079
|
};
|
|
80104
79080
|
};
|
|
80105
79081
|
const getRemoteFrame = async (handle) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame));
|
|
79082
|
+
const getNativeFullFrameRgb = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToFullFrame));
|
|
80106
79083
|
const fetchNativeFullFrameTiered = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame));
|
|
80107
|
-
const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
|
|
80108
79084
|
const cropMetricLogger = logger.child("NativeCrop");
|
|
80109
79085
|
let nativeHits = 0;
|
|
80110
79086
|
let nativeFallbacks = 0;
|
|
@@ -80120,8 +79096,8 @@ function createNativeFrameTransport(deps) {
|
|
|
80120
79096
|
/**
|
|
80121
79097
|
* Publish the window once `request` has SETTLED — never before, or the line
|
|
80122
79098
|
* carries a window whose own outcome has not been counted yet and throttles
|
|
80123
|
-
* away the one that has. `finally`, so the deliberate throw of
|
|
80124
|
-
*
|
|
79099
|
+
* away the one that has. `finally`, so the deliberate throw of the
|
|
79100
|
+
* full-frame fetches still reaches their callers.
|
|
80125
79101
|
*/
|
|
80126
79102
|
const closeWindowAfter = async (request) => {
|
|
80127
79103
|
try {
|
|
@@ -80178,7 +79154,7 @@ function createNativeFrameTransport(deps) {
|
|
|
80178
79154
|
try {
|
|
80179
79155
|
return await requestNativeCrop(frameHandle, bbox, maxWidth, normalize);
|
|
80180
79156
|
} catch (err) {
|
|
80181
|
-
cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg
|
|
79157
|
+
cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg(err) } });
|
|
80182
79158
|
return null;
|
|
80183
79159
|
}
|
|
80184
79160
|
};
|
|
@@ -80192,7 +79168,7 @@ function createNativeFrameTransport(deps) {
|
|
|
80192
79168
|
bumpCropMetric(false);
|
|
80193
79169
|
return null;
|
|
80194
79170
|
}
|
|
80195
|
-
if (
|
|
79171
|
+
if (native.tier === "ram-fullframe") {
|
|
80196
79172
|
bumpCropMetric(false);
|
|
80197
79173
|
cropMetricLogger.info("subject crop refused: sub-native tier (will retry, no masquerade)", {
|
|
80198
79174
|
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
@@ -80212,7 +79188,6 @@ function createNativeFrameTransport(deps) {
|
|
|
80212
79188
|
return {
|
|
80213
79189
|
getRemoteFrame,
|
|
80214
79190
|
getNativeFullFrameRgb,
|
|
80215
|
-
getNativeKeyFrameRgb: fetchNativeFullFrameTiered,
|
|
80216
79191
|
getNativeCropJpeg,
|
|
80217
79192
|
captureCrop: createCaptureCrop({
|
|
80218
79193
|
tryNativeCrop,
|
|
@@ -80226,7 +79201,7 @@ function createNativeFrameTransport(deps) {
|
|
|
80226
79201
|
const tiered = await fetchNativeFullFrameTiered(handle, DISPLAY_FALLBACK_MAX_WIDTH).catch((err) => {
|
|
80227
79202
|
logger.child("DisplayCrop").debug("fallback full-frame fetch failed — treating as a miss", { meta: {
|
|
80228
79203
|
nodeId: handle.nodeId,
|
|
80229
|
-
error: errMsg
|
|
79204
|
+
error: errMsg(err)
|
|
80230
79205
|
} });
|
|
80231
79206
|
return null;
|
|
80232
79207
|
});
|
|
@@ -80441,7 +79416,7 @@ function createParkedFrameClient(deps) {
|
|
|
80441
79416
|
tags: { deviceId: order.deviceId },
|
|
80442
79417
|
meta: {
|
|
80443
79418
|
...meta,
|
|
80444
|
-
error: errMsg
|
|
79419
|
+
error: errMsg(err)
|
|
80445
79420
|
}
|
|
80446
79421
|
});
|
|
80447
79422
|
return false;
|
|
@@ -80521,7 +79496,7 @@ function createParkedFrameClient(deps) {
|
|
|
80521
79496
|
trackId,
|
|
80522
79497
|
kind,
|
|
80523
79498
|
nodeId: known.nodeId,
|
|
80524
|
-
error: errMsg
|
|
79499
|
+
error: errMsg(err)
|
|
80525
79500
|
}
|
|
80526
79501
|
});
|
|
80527
79502
|
return null;
|
|
@@ -80543,7 +79518,7 @@ function createParkedFrameClient(deps) {
|
|
|
80543
79518
|
meta: {
|
|
80544
79519
|
trackId,
|
|
80545
79520
|
nodeId,
|
|
80546
|
-
error: errMsg
|
|
79521
|
+
error: errMsg(err)
|
|
80547
79522
|
}
|
|
80548
79523
|
});
|
|
80549
79524
|
});
|
|
@@ -82658,7 +81633,7 @@ async function projectSensorMarkers(deps, data, timestamp) {
|
|
|
82658
81633
|
capName: data.capName,
|
|
82659
81634
|
kind: descriptor.kind,
|
|
82660
81635
|
reason: "not-materialized",
|
|
82661
|
-
...outcome.status === "rejected" ? { error: errMsg
|
|
81636
|
+
...outcome.status === "rejected" ? { error: errMsg(outcome.reason) } : {}
|
|
82662
81637
|
});
|
|
82663
81638
|
return;
|
|
82664
81639
|
}
|
|
@@ -86288,947 +85263,6 @@ var RawGlobalStoreMirror = class {
|
|
|
86288
85263
|
}
|
|
86289
85264
|
};
|
|
86290
85265
|
//#endregion
|
|
86291
|
-
//#region src/pipeline-analytics/summary/group-crop.ts
|
|
86292
|
-
/** Padding around the union, as a fraction of its own size. */
|
|
86293
|
-
var GROUP_CROP_PADDING = .12;
|
|
86294
|
-
/**
|
|
86295
|
-
* Smallest crop, per axis, as a fraction of the frame.
|
|
86296
|
-
*
|
|
86297
|
-
* A quarter. Below that a thumbnail stops being legible at the sizes the
|
|
86298
|
-
* gallery draws it, and the extra context costs nothing — the subjects stay the
|
|
86299
|
-
* dominant thing in the picture at this size.
|
|
86300
|
-
*/
|
|
86301
|
-
var MIN_CROP_FRACTION = .25;
|
|
86302
|
-
/**
|
|
86303
|
-
* The crop, or `null` when there is nothing to cut to — no visible member, or
|
|
86304
|
-
* a frame with no dimensions. `null` means the caller keeps the full frame,
|
|
86305
|
-
* which is the only honest answer when nobody could say where the group was.
|
|
86306
|
-
*/
|
|
86307
|
-
function groupCropRect(input) {
|
|
86308
|
-
const { frameWidth: fw, frameHeight: fh } = input;
|
|
86309
|
-
if (fw <= 0 || fh <= 0) return null;
|
|
86310
|
-
const boxes = input.boxes.filter((b) => b.width > 0 && b.height > 0);
|
|
86311
|
-
if (boxes.length === 0) return null;
|
|
86312
|
-
let left = Number.POSITIVE_INFINITY;
|
|
86313
|
-
let top = Number.POSITIVE_INFINITY;
|
|
86314
|
-
let right = Number.NEGATIVE_INFINITY;
|
|
86315
|
-
let bottom = Number.NEGATIVE_INFINITY;
|
|
86316
|
-
for (const b of boxes) {
|
|
86317
|
-
left = Math.min(left, b.x);
|
|
86318
|
-
top = Math.min(top, b.y);
|
|
86319
|
-
right = Math.max(right, b.x + b.width);
|
|
86320
|
-
bottom = Math.max(bottom, b.y + b.height);
|
|
86321
|
-
}
|
|
86322
|
-
const padX = (right - left) * GROUP_CROP_PADDING;
|
|
86323
|
-
const padY = (bottom - top) * GROUP_CROP_PADDING;
|
|
86324
|
-
let x0 = left - padX;
|
|
86325
|
-
let y0 = top - padY;
|
|
86326
|
-
let x1 = right + padX;
|
|
86327
|
-
let y1 = bottom + padY;
|
|
86328
|
-
const minW = fw * MIN_CROP_FRACTION;
|
|
86329
|
-
const minH = fh * MIN_CROP_FRACTION;
|
|
86330
|
-
if (x1 - x0 < minW) {
|
|
86331
|
-
const cx = (x0 + x1) / 2;
|
|
86332
|
-
x0 = cx - minW / 2;
|
|
86333
|
-
x1 = cx + minW / 2;
|
|
86334
|
-
}
|
|
86335
|
-
if (y1 - y0 < minH) {
|
|
86336
|
-
const cy = (y0 + y1) / 2;
|
|
86337
|
-
y0 = cy - minH / 2;
|
|
86338
|
-
y1 = cy + minH / 2;
|
|
86339
|
-
}
|
|
86340
|
-
const width = Math.min(x1 - x0, fw);
|
|
86341
|
-
const height = Math.min(y1 - y0, fh);
|
|
86342
|
-
const x = Math.max(0, Math.min(x0, fw - width));
|
|
86343
|
-
const y = Math.max(0, Math.min(y0, fh - height));
|
|
86344
|
-
return {
|
|
86345
|
-
x: Math.round(x),
|
|
86346
|
-
y: Math.round(y),
|
|
86347
|
-
width: Math.round(width),
|
|
86348
|
-
height: Math.round(height)
|
|
86349
|
-
};
|
|
86350
|
-
}
|
|
86351
|
-
/**
|
|
86352
|
-
* Carry a crop computed on the DETECTION frame onto the NATIVE surface.
|
|
86353
|
-
*
|
|
86354
|
-
* The two are different sizes by construction: detection runs on a downscaled
|
|
86355
|
-
* frame and the group shot is fetched at up to 4096 px. Applying the rectangle
|
|
86356
|
-
* raw would cut a corner of the picture and call it the group — the failure
|
|
86357
|
-
* mode is silent and looks like a framing bug.
|
|
86358
|
-
*
|
|
86359
|
-
* Scaled per axis rather than by one factor, because the two surfaces are not
|
|
86360
|
-
* guaranteed to share an aspect, and clamped to the native frame so rounding
|
|
86361
|
-
* can never ask for a pixel that is not there.
|
|
86362
|
-
*/
|
|
86363
|
-
function scaleCropToFrame(crop, from, to) {
|
|
86364
|
-
if (from.width <= 0 || from.height <= 0 || to.width <= 0 || to.height <= 0) return null;
|
|
86365
|
-
const sx = to.width / from.width;
|
|
86366
|
-
const sy = to.height / from.height;
|
|
86367
|
-
const x = Math.max(0, Math.min(Math.round(crop.x * sx), to.width - 1));
|
|
86368
|
-
const y = Math.max(0, Math.min(Math.round(crop.y * sy), to.height - 1));
|
|
86369
|
-
return {
|
|
86370
|
-
x,
|
|
86371
|
-
y,
|
|
86372
|
-
width: Math.max(1, Math.min(Math.round(crop.width * sx), to.width - x)),
|
|
86373
|
-
height: Math.max(1, Math.min(Math.round(crop.height * sy), to.height - y))
|
|
86374
|
-
};
|
|
86375
|
-
}
|
|
86376
|
-
//#endregion
|
|
86377
|
-
//#region src/pipeline-analytics/summary/summary-score.ts
|
|
86378
|
-
/** A member whose own box is covered this much by others is hidden. */
|
|
86379
|
-
var OCCLUDED_COVER_FRACTION = .5;
|
|
86380
|
-
/** A box covering this share of the frame scores full marks on area. */
|
|
86381
|
-
var FULL_AREA_SHARE = .25;
|
|
86382
|
-
var WEIGHT_CONFIDENCE = .3;
|
|
86383
|
-
var WEIGHT_AREA = .25;
|
|
86384
|
-
var WEIGHT_RECOGNITION = .25;
|
|
86385
|
-
var WEIGHT_PLACEMENT = .2;
|
|
86386
|
-
function intersectionArea(a, b) {
|
|
86387
|
-
const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
|
|
86388
|
-
const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
86389
|
-
return w > 0 && h > 0 ? w * h : 0;
|
|
86390
|
-
}
|
|
86391
|
-
/**
|
|
86392
|
-
* Share of `own` covered by `others`, 0..1. Pairwise intersections summed and
|
|
86393
|
-
* capped at the own area: when two others overlap each other on top of this
|
|
86394
|
-
* box the sum over-counts, which only ever calls a member MORE hidden, never
|
|
86395
|
-
* less — the safe side for a shot meant to show everyone.
|
|
86396
|
-
*/
|
|
86397
|
-
function coveredFraction(own, others) {
|
|
86398
|
-
const area = own.width * own.height;
|
|
86399
|
-
if (area <= 0) return 1;
|
|
86400
|
-
const covered = others.reduce((sum, other) => sum + intersectionArea(own, other), 0);
|
|
86401
|
-
return Math.min(1, covered / area);
|
|
86402
|
-
}
|
|
86403
|
-
function clamp01(value) {
|
|
86404
|
-
return Math.max(0, Math.min(1, value));
|
|
86405
|
-
}
|
|
86406
|
-
function placement(box, frameWidth, frameHeight) {
|
|
86407
|
-
const cx = box.x + box.width / 2;
|
|
86408
|
-
const cy = box.y + box.height / 2;
|
|
86409
|
-
const dx = cx - frameWidth / 2;
|
|
86410
|
-
const dy = cy - frameHeight / 2;
|
|
86411
|
-
const halfDiagonal = Math.hypot(frameWidth, frameHeight) / 2;
|
|
86412
|
-
return halfDiagonal <= 0 ? 0 : 1 - clamp01(Math.hypot(dx, dy) / halfDiagonal);
|
|
86413
|
-
}
|
|
86414
|
-
function memberQuality(d, frameWidth, frameHeight) {
|
|
86415
|
-
const frameArea = frameWidth * frameHeight;
|
|
86416
|
-
const cappedArea = clamp01((frameArea <= 0 ? 0 : d.bbox.width * d.bbox.height / frameArea) / FULL_AREA_SHARE);
|
|
86417
|
-
return WEIGHT_CONFIDENCE * clamp01(d.confidence) + WEIGHT_AREA * cappedArea + WEIGHT_RECOGNITION * (d.recognized ? 1 : 0) + WEIGHT_PLACEMENT * placement(d.bbox, frameWidth, frameHeight);
|
|
86418
|
-
}
|
|
86419
|
-
/** Score one frame for one summary. Pure; never throws on empty input. */
|
|
86420
|
-
function scoreSummaryFrame(input) {
|
|
86421
|
-
const present = input.detections.filter((d) => input.members.has(d.trackId));
|
|
86422
|
-
const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
|
|
86423
|
-
if (visible.length === 0) return {
|
|
86424
|
-
visible: 0,
|
|
86425
|
-
quality: 0,
|
|
86426
|
-
crop: null
|
|
86427
|
-
};
|
|
86428
|
-
const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
|
|
86429
|
-
const crop = groupCropRect({
|
|
86430
|
-
boxes: visible.map((d) => ({
|
|
86431
|
-
x: d.bbox.x,
|
|
86432
|
-
y: d.bbox.y,
|
|
86433
|
-
width: d.bbox.width,
|
|
86434
|
-
height: d.bbox.height
|
|
86435
|
-
})),
|
|
86436
|
-
frameWidth: input.frameWidth,
|
|
86437
|
-
frameHeight: input.frameHeight
|
|
86438
|
-
});
|
|
86439
|
-
return {
|
|
86440
|
-
visible: visible.length,
|
|
86441
|
-
quality,
|
|
86442
|
-
crop
|
|
86443
|
-
};
|
|
86444
|
-
}
|
|
86445
|
-
/**
|
|
86446
|
-
* Lexicographic: more visible members wins outright; only equal counts fall
|
|
86447
|
-
* through to quality. Positive when `a` is the better shot.
|
|
86448
|
-
*/
|
|
86449
|
-
function compareSummaryScores(a, b) {
|
|
86450
|
-
if (a.visible !== b.visible) return a.visible - b.visible;
|
|
86451
|
-
return a.quality - b.quality;
|
|
86452
|
-
}
|
|
86453
|
-
var GroupShotTracker = class {
|
|
86454
|
-
states = /* @__PURE__ */ new Map();
|
|
86455
|
-
offered = 0;
|
|
86456
|
-
materialised = 0;
|
|
86457
|
-
byGap = 0;
|
|
86458
|
-
byMargin = 0;
|
|
86459
|
-
byWorse = 0;
|
|
86460
|
-
/**
|
|
86461
|
-
* Offer a frame. Returns `true` when it is worth materialising NOW — the
|
|
86462
|
-
* caller's cue to fetch the pixels for THIS frame, while its handle is
|
|
86463
|
-
* still leased.
|
|
86464
|
-
*
|
|
86465
|
-
* The clock is `frame.timestamp`; the caller injects no second one.
|
|
86466
|
-
*/
|
|
86467
|
-
offer(summaryId, score, frame) {
|
|
86468
|
-
if (score.visible === 0) return false;
|
|
86469
|
-
this.offered += 1;
|
|
86470
|
-
const state = this.states.get(summaryId);
|
|
86471
|
-
if (state === void 0) {
|
|
86472
|
-
this.states.set(summaryId, {
|
|
86473
|
-
delivered: null,
|
|
86474
|
-
lastAttemptAt: frame.timestamp,
|
|
86475
|
-
unlanded: 1
|
|
86476
|
-
});
|
|
86477
|
-
this.materialised += 1;
|
|
86478
|
-
return true;
|
|
86479
|
-
}
|
|
86480
|
-
if (state.lastAttemptAt !== void 0 && frame.timestamp - state.lastAttemptAt < 2e3) {
|
|
86481
|
-
this.byGap += 1;
|
|
86482
|
-
return false;
|
|
86483
|
-
}
|
|
86484
|
-
if (!this.isWorthPixels(state, score)) return false;
|
|
86485
|
-
state.lastAttemptAt = frame.timestamp;
|
|
86486
|
-
state.unlanded += 1;
|
|
86487
|
-
this.materialised += 1;
|
|
86488
|
-
return true;
|
|
86489
|
-
}
|
|
86490
|
-
/**
|
|
86491
|
-
* Nothing has landed yet → keep trying (D58) until the budget is spent.
|
|
86492
|
-
* Something has landed → beat it, by the margin on the quality tiebreak.
|
|
86493
|
-
*/
|
|
86494
|
-
isWorthPixels(state, score) {
|
|
86495
|
-
const delivered = state.delivered;
|
|
86496
|
-
if (delivered === null) {
|
|
86497
|
-
if (state.unlanded >= 4) {
|
|
86498
|
-
this.byMargin += 1;
|
|
86499
|
-
return false;
|
|
86500
|
-
}
|
|
86501
|
-
return true;
|
|
86502
|
-
}
|
|
86503
|
-
if (compareSummaryScores(score, delivered.score) <= 0) {
|
|
86504
|
-
this.byWorse += 1;
|
|
86505
|
-
return false;
|
|
86506
|
-
}
|
|
86507
|
-
if (score.visible === delivered.score.visible && score.quality - delivered.score.quality < .05) {
|
|
86508
|
-
this.byMargin += 1;
|
|
86509
|
-
return false;
|
|
86510
|
-
}
|
|
86511
|
-
return true;
|
|
86512
|
-
}
|
|
86513
|
-
/**
|
|
86514
|
-
* A materialisation LANDED. Only this advances the baseline the next
|
|
86515
|
-
* candidate must beat — a fetch that missed leaves the summary exactly as
|
|
86516
|
-
* picture-less as it was, and the next frame may be worse and still worth
|
|
86517
|
-
* taking.
|
|
86518
|
-
*/
|
|
86519
|
-
noteLanded(summaryId, score, frame) {
|
|
86520
|
-
const state = this.states.get(summaryId);
|
|
86521
|
-
if (state === void 0) return;
|
|
86522
|
-
state.delivered = {
|
|
86523
|
-
score,
|
|
86524
|
-
frame
|
|
86525
|
-
};
|
|
86526
|
-
state.unlanded = 0;
|
|
86527
|
-
}
|
|
86528
|
-
/** The shot on disk for this summary, as this process last saw it land. */
|
|
86529
|
-
deliveredOf(summaryId) {
|
|
86530
|
-
return this.states.get(summaryId)?.delivered ?? null;
|
|
86531
|
-
}
|
|
86532
|
-
/** A sealed or evicted summary takes no more frames; drop its state. */
|
|
86533
|
-
forget(summaryId) {
|
|
86534
|
-
this.states.delete(summaryId);
|
|
86535
|
-
}
|
|
86536
|
-
/**
|
|
86537
|
-
* What the gate did since the last read. Shipped WITH the gate: without it
|
|
86538
|
-
* the only evidence for a fetch that did NOT happen is hub-main's byte
|
|
86539
|
-
* counter, which answers a different question a minute later.
|
|
86540
|
-
*/
|
|
86541
|
-
drainCounts() {
|
|
86542
|
-
const counts = {
|
|
86543
|
-
offered: this.offered,
|
|
86544
|
-
materialised: this.materialised,
|
|
86545
|
-
byGap: this.byGap,
|
|
86546
|
-
byMargin: this.byMargin,
|
|
86547
|
-
byWorse: this.byWorse
|
|
86548
|
-
};
|
|
86549
|
-
this.offered = 0;
|
|
86550
|
-
this.materialised = 0;
|
|
86551
|
-
this.byGap = 0;
|
|
86552
|
-
this.byMargin = 0;
|
|
86553
|
-
this.byWorse = 0;
|
|
86554
|
-
return counts;
|
|
86555
|
-
}
|
|
86556
|
-
get size() {
|
|
86557
|
-
return this.states.size;
|
|
86558
|
-
}
|
|
86559
|
-
};
|
|
86560
|
-
//#endregion
|
|
86561
|
-
//#region src/pipeline-analytics/summary/group-shot-materialiser.ts
|
|
86562
|
-
/**
|
|
86563
|
-
* Apply the group crop to the native surface, or hand it back untouched.
|
|
86564
|
-
*
|
|
86565
|
-
* Every reason to skip is a reason NOT to guess: no rectangle, no detection
|
|
86566
|
-
* geometry to scale from, or a cropper this deployment does not have. The full
|
|
86567
|
-
* frame is worse than a crop and far better than a wrong crop.
|
|
86568
|
-
*/
|
|
86569
|
-
async function cropNative(native, input, deps) {
|
|
86570
|
-
const crop = input.crop;
|
|
86571
|
-
if (!crop || !deps.cropFrame) return native;
|
|
86572
|
-
const fw = input.cropFrameWidth;
|
|
86573
|
-
const fh = input.cropFrameHeight;
|
|
86574
|
-
if (fw === void 0 || fh === void 0) return native;
|
|
86575
|
-
const scaled = scaleCropToFrame(crop, {
|
|
86576
|
-
width: fw,
|
|
86577
|
-
height: fh
|
|
86578
|
-
}, native);
|
|
86579
|
-
if (scaled === null) return native;
|
|
86580
|
-
if (scaled.width >= native.width && scaled.height >= native.height) return native;
|
|
86581
|
-
try {
|
|
86582
|
-
const cut = await deps.cropFrame(native, scaled);
|
|
86583
|
-
if (cut === null) return native;
|
|
86584
|
-
const expected = cut.width * cut.height * 3;
|
|
86585
|
-
if (cut.format === "rgb" && cut.data.length !== expected) {
|
|
86586
|
-
deps.logger.warn("group shot crop returned a surface that is not its geometry", {
|
|
86587
|
-
tags: { deviceId: input.deviceId },
|
|
86588
|
-
meta: {
|
|
86589
|
-
summaryId: input.summaryId,
|
|
86590
|
-
width: cut.width,
|
|
86591
|
-
height: cut.height,
|
|
86592
|
-
expectedBytes: expected,
|
|
86593
|
-
gotBytes: cut.data.length
|
|
86594
|
-
}
|
|
86595
|
-
});
|
|
86596
|
-
return native;
|
|
86597
|
-
}
|
|
86598
|
-
return cut;
|
|
86599
|
-
} catch (err) {
|
|
86600
|
-
deps.logger.warn("group shot crop failed — storing the full frame", {
|
|
86601
|
-
tags: { deviceId: input.deviceId },
|
|
86602
|
-
meta: {
|
|
86603
|
-
summaryId: input.summaryId,
|
|
86604
|
-
error: errMsg$1(err)
|
|
86605
|
-
}
|
|
86606
|
-
});
|
|
86607
|
-
return native;
|
|
86608
|
-
}
|
|
86609
|
-
}
|
|
86610
|
-
async function persistSummaryGroupShot(deps, input) {
|
|
86611
|
-
const { deviceId, summaryId, frameHandle } = input;
|
|
86612
|
-
const drop = {
|
|
86613
|
-
tags: { deviceId },
|
|
86614
|
-
meta: {
|
|
86615
|
-
summaryId,
|
|
86616
|
-
nodeId: frameHandle.nodeId
|
|
86617
|
-
}
|
|
86618
|
-
};
|
|
86619
|
-
try {
|
|
86620
|
-
const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
|
|
86621
|
-
const native = result?.frame;
|
|
86622
|
-
if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
|
|
86623
|
-
deps.logger.warn("group shot native miss — the previous shot stands", {
|
|
86624
|
-
...drop,
|
|
86625
|
-
meta: {
|
|
86626
|
-
...drop.meta,
|
|
86627
|
-
shmId: frameHandle.shmId
|
|
86628
|
-
}
|
|
86629
|
-
});
|
|
86630
|
-
return { kind: "native-miss" };
|
|
86631
|
-
}
|
|
86632
|
-
if (!keyFrameAcceptsTier(result.tier)) {
|
|
86633
|
-
deps.logger.warn("group shot RAM tier rejected — the previous shot stands", {
|
|
86634
|
-
...drop,
|
|
86635
|
-
meta: {
|
|
86636
|
-
...drop.meta,
|
|
86637
|
-
shmId: frameHandle.shmId,
|
|
86638
|
-
tier: result.tier
|
|
86639
|
-
}
|
|
86640
|
-
});
|
|
86641
|
-
return { kind: "ram-tier" };
|
|
86642
|
-
}
|
|
86643
|
-
const surface = await cropNative(native, input, deps);
|
|
86644
|
-
const { keyFrame, keyFrameSmall } = await deps.encodeVariants(surface);
|
|
86645
|
-
const keyFrameMediaId = await deps.media.putReplacing({
|
|
86646
|
-
deviceId,
|
|
86647
|
-
ownerKind: "summary",
|
|
86648
|
-
ownerId: summaryId,
|
|
86649
|
-
kind: "keyFrame",
|
|
86650
|
-
timestamp: input.timestamp,
|
|
86651
|
-
data: keyFrame
|
|
86652
|
-
});
|
|
86653
|
-
try {
|
|
86654
|
-
await deps.media.putReplacing({
|
|
86655
|
-
deviceId,
|
|
86656
|
-
ownerKind: "summary",
|
|
86657
|
-
ownerId: summaryId,
|
|
86658
|
-
kind: "keyFrameSmall",
|
|
86659
|
-
timestamp: input.timestamp,
|
|
86660
|
-
data: keyFrameSmall
|
|
86661
|
-
});
|
|
86662
|
-
} catch (err) {
|
|
86663
|
-
deps.logger.warn("group shot web companion write failed — the native shot is stored", {
|
|
86664
|
-
...drop,
|
|
86665
|
-
meta: {
|
|
86666
|
-
...drop.meta,
|
|
86667
|
-
error: errMsg$1(err)
|
|
86668
|
-
}
|
|
86669
|
-
});
|
|
86670
|
-
}
|
|
86671
|
-
return {
|
|
86672
|
-
kind: "landed",
|
|
86673
|
-
keyFrameMediaId
|
|
86674
|
-
};
|
|
86675
|
-
} catch (err) {
|
|
86676
|
-
const error = errMsg$1(err);
|
|
86677
|
-
deps.logger.warn("group shot capture failed", {
|
|
86678
|
-
...drop,
|
|
86679
|
-
meta: {
|
|
86680
|
-
...drop.meta,
|
|
86681
|
-
error
|
|
86682
|
-
}
|
|
86683
|
-
});
|
|
86684
|
-
return {
|
|
86685
|
-
kind: "error",
|
|
86686
|
-
error
|
|
86687
|
-
};
|
|
86688
|
-
}
|
|
86689
|
-
}
|
|
86690
|
-
//#endregion
|
|
86691
|
-
//#region src/pipeline-analytics/summary/summary-composition.ts
|
|
86692
|
-
function summaryComposition(members) {
|
|
86693
|
-
const counts = /* @__PURE__ */ new Map();
|
|
86694
|
-
for (const member of members) counts.set(member.className, (counts.get(member.className) ?? 0) + 1);
|
|
86695
|
-
return { classes: [...counts].map(([className, count]) => ({
|
|
86696
|
-
className,
|
|
86697
|
-
count
|
|
86698
|
-
})).toSorted((a, b) => a.count !== b.count ? b.count - a.count : (a.className ?? "").localeCompare(b.className ?? "")) };
|
|
86699
|
-
}
|
|
86700
|
-
//#endregion
|
|
86701
|
-
//#region src/pipeline-analytics/summary/summary-member-labels.ts
|
|
86702
|
-
function present(value) {
|
|
86703
|
-
return typeof value === "string" && value.length > 0;
|
|
86704
|
-
}
|
|
86705
|
-
function withMemberLabels(members, tracks) {
|
|
86706
|
-
const byId = new Map(tracks.map((t) => [t.trackId, t]));
|
|
86707
|
-
return members.map((member) => {
|
|
86708
|
-
const track = byId.get(member.trackId);
|
|
86709
|
-
return {
|
|
86710
|
-
...member,
|
|
86711
|
-
className: member.className ?? (present(track?.className) ? track.className : null),
|
|
86712
|
-
...present(track?.label) ? { label: track.label } : {},
|
|
86713
|
-
...present(track?.subLabel) ? { subLabel: track.subLabel } : {}
|
|
86714
|
-
};
|
|
86715
|
-
});
|
|
86716
|
-
}
|
|
86717
|
-
//#endregion
|
|
86718
|
-
//#region src/pipeline-analytics/summary/summary-proximity.ts
|
|
86719
|
-
/**
|
|
86720
|
-
* The measured gap, in fractions of the frame's diagonal-free normalisation
|
|
86721
|
-
* (x by width, y by height). See the arithmetic above before moving it.
|
|
86722
|
-
*/
|
|
86723
|
-
var DEFAULT_TOGETHER_FRACTION = .15;
|
|
86724
|
-
/**
|
|
86725
|
-
* Distance between two points as a fraction of the frame, or `null` when the
|
|
86726
|
-
* frame is unknown. Each axis is normalised by its OWN dimension, so the answer
|
|
86727
|
-
* does not change when a camera's aspect does.
|
|
86728
|
-
*/
|
|
86729
|
-
function normalisedDistance(a, b, frame) {
|
|
86730
|
-
if (!(frame.width > 0) || !(frame.height > 0)) return null;
|
|
86731
|
-
const dx = (a.x - b.x) / frame.width;
|
|
86732
|
-
const dy = (a.y - b.y) / frame.height;
|
|
86733
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
86734
|
-
}
|
|
86735
|
-
//#endregion
|
|
86736
|
-
//#region src/pipeline-analytics/summary/summary-join.ts
|
|
86737
|
-
/**
|
|
86738
|
-
* Which open group a newborn track joins — or none, and it opens its own.
|
|
86739
|
-
*
|
|
86740
|
-
* ## Why this is a separate decision from the store
|
|
86741
|
-
*
|
|
86742
|
-
* The store knows rows; only the caller can see where subjects ARE. Keeping the
|
|
86743
|
-
* choice here means the store's `listOpen` + `absorbInto` stay a mechanism and
|
|
86744
|
-
* this stays the policy, which is what let the policy change without the store
|
|
86745
|
-
* changing underneath it.
|
|
86746
|
-
*
|
|
86747
|
-
* ## The two cases this exists for
|
|
86748
|
-
*
|
|
86749
|
-
* A summary used to be one per camera, so a subject at the far end of the drive
|
|
86750
|
-
* was absorbed into the group at the door — a group it was never part of — and
|
|
86751
|
-
* the halves of a group that split had nowhere to go. Both are the same need:
|
|
86752
|
-
* a summary is a GROUP, not a camera session.
|
|
86753
|
-
*
|
|
86754
|
-
* ## Nearest, not first
|
|
86755
|
-
*
|
|
86756
|
-
* When two groups are within reach the closest wins. `listOpen` orders by
|
|
86757
|
-
* `openedAt`, and which group opened first has nothing to do with who this
|
|
86758
|
-
* subject is walking with — taking the first would make the answer depend on
|
|
86759
|
-
* list order, which is the kind of tie-break that reads as random in a log.
|
|
86760
|
-
*
|
|
86761
|
-
* ## Knowing nothing is not a reason to take somebody in
|
|
86762
|
-
*
|
|
86763
|
-
* A group whose members have no position in RAM — one open across a restart —
|
|
86764
|
-
* is skipped rather than joined. Joining on absent evidence is exactly how a
|
|
86765
|
-
* far subject ends up in the wrong group, and nothing revisits that decision.
|
|
86766
|
-
*/
|
|
86767
|
-
/**
|
|
86768
|
-
* The id of the group to join, or `null` to open a new one.
|
|
86769
|
-
*
|
|
86770
|
-
* `null` on an unknown frame too: refusing is the recoverable direction, since
|
|
86771
|
-
* the subject then gets its own group, while a wrong join merges two strangers.
|
|
86772
|
-
*/
|
|
86773
|
-
function chooseGroupToJoin(subject, groups, frame, maxFraction) {
|
|
86774
|
-
let bestId = null;
|
|
86775
|
-
let bestDistance = Number.POSITIVE_INFINITY;
|
|
86776
|
-
for (const g of groups) for (const m of g.memberPositions) {
|
|
86777
|
-
const d = normalisedDistance(subject, m, frame);
|
|
86778
|
-
if (d === null || d > maxFraction) continue;
|
|
86779
|
-
if (d < bestDistance) {
|
|
86780
|
-
bestDistance = d;
|
|
86781
|
-
bestId = g.id;
|
|
86782
|
-
}
|
|
86783
|
-
}
|
|
86784
|
-
return bestId;
|
|
86785
|
-
}
|
|
86786
|
-
//#endregion
|
|
86787
|
-
//#region src/pipeline-analytics/summary/summary-split.ts
|
|
86788
|
-
/**
|
|
86789
|
-
* A group that walked apart — who leaves, and who keeps the summary.
|
|
86790
|
-
*
|
|
86791
|
-
* ## The case
|
|
86792
|
-
*
|
|
86793
|
-
* Membership is decided ONCE, at birth, by proximity (D489). That answers "who
|
|
86794
|
-
* arrived together" and says nothing about what happens next: two people who
|
|
86795
|
-
* meet at the gate and then go separate ways stay one group until the camera
|
|
86796
|
-
* falls quiet, and the summary then claims they were together for the whole
|
|
86797
|
-
* session. The operator asked for this case by name alongside the concurrent
|
|
86798
|
-
* ones — "un gruppo che si divide" — and they are the same need, because the
|
|
86799
|
-
* halves of a split have to be able to BE groups.
|
|
86800
|
-
*
|
|
86801
|
-
* ## Single linkage, deliberately
|
|
86802
|
-
*
|
|
86803
|
-
* A family in single file is a chain: the last of them is nowhere near the
|
|
86804
|
-
* first, and requiring every pair to be close would tear the group apart the
|
|
86805
|
-
* moment it grew past two. So two members are together when a chain of
|
|
86806
|
-
* within-threshold hops connects them — the same "any, not all" rule
|
|
86807
|
-
* {@link isTogetherWith} uses at birth, applied to the whole set.
|
|
86808
|
-
*
|
|
86809
|
-
* ## What an absent position means
|
|
86810
|
-
*
|
|
86811
|
-
* Nothing. A member whose track has closed has no live position, and that is
|
|
86812
|
-
* not evidence it walked off — it is the ordinary end of a track. Unplaced
|
|
86813
|
-
* members are never candidates to leave; they stay with whoever keeps the
|
|
86814
|
-
* summary. Likewise an unknown frame yields a `null` distance, and a null is
|
|
86815
|
-
* not "far": it produces ONE cluster, never a split (D393).
|
|
86816
|
-
*
|
|
86817
|
-
* ## Which side keeps the row
|
|
86818
|
-
*
|
|
86819
|
-
* The primary's cluster, because the summary's identity and its group shot are
|
|
86820
|
-
* already the primary's. When the primary cannot be placed — its track closed —
|
|
86821
|
-
* the largest cluster keeps it instead; refusing to split there would freeze
|
|
86822
|
-
* every group the moment its primary left the frame, which is the common way
|
|
86823
|
-
* for a group to break up in the first place.
|
|
86824
|
-
*/
|
|
86825
|
-
/**
|
|
86826
|
-
* Members grouped by single-linkage proximity. One cluster when nothing can be
|
|
86827
|
-
* measured — an unknown frame is not a reason to tear a group in half.
|
|
86828
|
-
*/
|
|
86829
|
-
function clusterByProximity(members, frame, maxFraction) {
|
|
86830
|
-
const parent = members.map((_, i) => i);
|
|
86831
|
-
const find = (i) => {
|
|
86832
|
-
let root = i;
|
|
86833
|
-
while (parent[root] !== root) root = parent[root];
|
|
86834
|
-
return root;
|
|
86835
|
-
};
|
|
86836
|
-
const union = (a, b) => {
|
|
86837
|
-
const ra = find(a);
|
|
86838
|
-
const rb = find(b);
|
|
86839
|
-
if (ra !== rb) parent[rb] = ra;
|
|
86840
|
-
};
|
|
86841
|
-
let measured = false;
|
|
86842
|
-
for (let i = 0; i < members.length; i++) for (let j = i + 1; j < members.length; j++) {
|
|
86843
|
-
const d = normalisedDistance(members[i].position, members[j].position, frame);
|
|
86844
|
-
if (d === null) continue;
|
|
86845
|
-
measured = true;
|
|
86846
|
-
if (d <= maxFraction) union(i, j);
|
|
86847
|
-
}
|
|
86848
|
-
if (!measured && members.length > 1) return [members.map((m) => m.trackId)];
|
|
86849
|
-
const byRoot = /* @__PURE__ */ new Map();
|
|
86850
|
-
for (let i = 0; i < members.length; i++) {
|
|
86851
|
-
const root = find(i);
|
|
86852
|
-
const bucket = byRoot.get(root);
|
|
86853
|
-
const trackId = members[i].trackId;
|
|
86854
|
-
if (bucket === void 0) byRoot.set(root, [trackId]);
|
|
86855
|
-
else bucket.push(trackId);
|
|
86856
|
-
}
|
|
86857
|
-
return [...byRoot.values()];
|
|
86858
|
-
}
|
|
86859
|
-
/**
|
|
86860
|
-
* The departures, or `null` when the group is still whole.
|
|
86861
|
-
*
|
|
86862
|
-
* Order is the input's: the clusters come out in the order their first member
|
|
86863
|
-
* appears, so a caller logging a split reads the same order twice running.
|
|
86864
|
-
*/
|
|
86865
|
-
function planSplit(subject, frame, maxFraction) {
|
|
86866
|
-
if (subject.placed.length < 2) return null;
|
|
86867
|
-
const clusters = clusterByProximity(subject.placed, frame, maxFraction);
|
|
86868
|
-
if (clusters.length < 2) return null;
|
|
86869
|
-
const keeper = clusters.find((c) => subject.primaryTrackId !== null && c.includes(subject.primaryTrackId)) ?? largest(clusters);
|
|
86870
|
-
return { departures: clusters.filter((c) => c !== keeper) };
|
|
86871
|
-
}
|
|
86872
|
-
/** The biggest cluster; ties go to the one that appeared first. */
|
|
86873
|
-
function largest(clusters) {
|
|
86874
|
-
let best = clusters[0];
|
|
86875
|
-
for (const c of clusters) if (c.length > best.length) best = c;
|
|
86876
|
-
return best;
|
|
86877
|
-
}
|
|
86878
|
-
function errMsg(err) {
|
|
86879
|
-
return err instanceof Error ? err.message : String(err);
|
|
86880
|
-
}
|
|
86881
|
-
var SummarySealer = class {
|
|
86882
|
-
deps;
|
|
86883
|
-
sweepInFlight = false;
|
|
86884
|
-
/**
|
|
86885
|
-
* What the FRAME path may ask at frame-rate: EVERY open group of a camera,
|
|
86886
|
-
* with the members it has seen THIS process open. Fed only by this class's
|
|
86887
|
-
* own writes, never the authority (D224): a fresh process answers an empty
|
|
86888
|
-
* list until a track opens, and a summary open across a restart is scored on
|
|
86889
|
-
* the members that open after it — a weaker shot, never a wrong one.
|
|
86890
|
-
*
|
|
86891
|
-
* Keyed by summary id INSIDE the camera, because a camera holds as many
|
|
86892
|
-
* groups as it has (D489). It was one entry per camera for one release, and
|
|
86893
|
-
* that silently threw away the group shot of every group but the last to
|
|
86894
|
-
* open — the frame path scored the newest group's members and no other.
|
|
86895
|
-
*/
|
|
86896
|
-
openMirror = /* @__PURE__ */ new Map();
|
|
86897
|
-
/**
|
|
86898
|
-
* When each open group was FIRST seen divided, by summary id. Cleared the
|
|
86899
|
-
* moment it is whole again, so a separation has to hold without interruption
|
|
86900
|
-
* — a group that oscillates never accumulates its window.
|
|
86901
|
-
*
|
|
86902
|
-
* RAM only, and deliberately: after a restart a group that is still apart
|
|
86903
|
-
* simply waits {@link SPLIT_CONFIRM_MS} again. Persisting it would buy one
|
|
86904
|
-
* earlier split and cost a column nothing else reads.
|
|
86905
|
-
*/
|
|
86906
|
-
apartSince = /* @__PURE__ */ new Map();
|
|
86907
|
-
constructor(deps) {
|
|
86908
|
-
this.deps = deps;
|
|
86909
|
-
}
|
|
86910
|
-
/** See {@link SUMMARY_GROUPS_ENABLED}; production never overrides it. */
|
|
86911
|
-
get groupsEnabled() {
|
|
86912
|
-
return this.deps.groupsEnabled ?? false;
|
|
86913
|
-
}
|
|
86914
|
-
/**
|
|
86915
|
-
* The camera's open groups, oldest entry first. Empty when none is mirrored —
|
|
86916
|
-
* which means "this process has opened none", not "the camera has none".
|
|
86917
|
-
*/
|
|
86918
|
-
openGroupsFor(deviceId) {
|
|
86919
|
-
const groups = this.openMirror.get(deviceId);
|
|
86920
|
-
if (groups === void 0) return [];
|
|
86921
|
-
return [...groups].map(([id, members]) => ({
|
|
86922
|
-
id,
|
|
86923
|
-
members
|
|
86924
|
-
}));
|
|
86925
|
-
}
|
|
86926
|
-
/**
|
|
86927
|
-
* Join the open group this subject is walking WITH, or open its own.
|
|
86928
|
-
*
|
|
86929
|
-
* Falls back to the store's own policy (the camera's oldest open group)
|
|
86930
|
-
* whenever the evidence is missing — no birth position, no frame size, no
|
|
86931
|
-
* member positions. That fallback is today's behaviour, so an unknowable
|
|
86932
|
-
* case degrades to what shipped rather than to nothing.
|
|
86933
|
-
*/
|
|
86934
|
-
async joinOrOpen(input, position) {
|
|
86935
|
-
const frame = this.deps.frameSize(input.deviceId);
|
|
86936
|
-
if (position === void 0 || frame === null) return await this.deps.summaries.openOrAbsorb(input);
|
|
86937
|
-
const open = await this.deps.summaries.listOpen(input.deviceId);
|
|
86938
|
-
if (open.length === 0) return await this.deps.summaries.openNew(input);
|
|
86939
|
-
const groups = [];
|
|
86940
|
-
for (const summary of open) {
|
|
86941
|
-
const members = await this.deps.summaries.membersOf(summary.id);
|
|
86942
|
-
const positions = await this.deps.latestPositions(members.map((m) => m.trackId));
|
|
86943
|
-
groups.push({
|
|
86944
|
-
id: summary.id,
|
|
86945
|
-
memberPositions: [...positions.values()]
|
|
86946
|
-
});
|
|
86947
|
-
}
|
|
86948
|
-
const chosenId = chooseGroupToJoin(position, groups, frame, this.deps.togetherFraction(input.deviceId));
|
|
86949
|
-
const chosen = open.find((s) => s.id === chosenId);
|
|
86950
|
-
if (chosen === void 0) {
|
|
86951
|
-
this.deps.logger.debug("summary: nobody to walk with — opening a group", {
|
|
86952
|
-
tags: { deviceId: input.deviceId },
|
|
86953
|
-
meta: {
|
|
86954
|
-
trackId: input.trackId,
|
|
86955
|
-
openGroups: open.length
|
|
86956
|
-
}
|
|
86957
|
-
});
|
|
86958
|
-
return await this.deps.summaries.openNew(input);
|
|
86959
|
-
}
|
|
86960
|
-
return await this.deps.summaries.absorbInto(chosen, input);
|
|
86961
|
-
}
|
|
86962
|
-
async onTrackOpened(input) {
|
|
86963
|
-
if (!this.groupsEnabled) return;
|
|
86964
|
-
try {
|
|
86965
|
-
if (!isSummaryEligible(input.trackId, input.className)) {
|
|
86966
|
-
this.deps.logger.debug("summary: track is not a group member — left standalone", {
|
|
86967
|
-
tags: { deviceId: input.deviceId },
|
|
86968
|
-
meta: {
|
|
86969
|
-
trackId: input.trackId,
|
|
86970
|
-
className: input.className ?? null
|
|
86971
|
-
}
|
|
86972
|
-
});
|
|
86973
|
-
return;
|
|
86974
|
-
}
|
|
86975
|
-
const at = this.deps.now();
|
|
86976
|
-
const open = {
|
|
86977
|
-
deviceId: input.deviceId,
|
|
86978
|
-
trackId: input.trackId,
|
|
86979
|
-
at,
|
|
86980
|
-
className: input.className
|
|
86981
|
-
};
|
|
86982
|
-
const row = await this.joinOrOpen(open, input.position);
|
|
86983
|
-
this.rememberMember(input.deviceId, row.id, input.trackId);
|
|
86984
|
-
} catch (err) {
|
|
86985
|
-
this.deps.logger.warn("summary: could not open or absorb for a track", {
|
|
86986
|
-
tags: { deviceId: input.deviceId },
|
|
86987
|
-
meta: {
|
|
86988
|
-
trackId: input.trackId,
|
|
86989
|
-
error: errMsg(err)
|
|
86990
|
-
}
|
|
86991
|
-
});
|
|
86992
|
-
}
|
|
86993
|
-
}
|
|
86994
|
-
/** One sweep at a time (THE latch). A tick that finds one in flight returns. */
|
|
86995
|
-
async sweep() {
|
|
86996
|
-
if (this.sweepInFlight) return;
|
|
86997
|
-
this.sweepInFlight = true;
|
|
86998
|
-
try {
|
|
86999
|
-
const mine = this.deps.analysedDeviceIds();
|
|
87000
|
-
const open = [];
|
|
87001
|
-
for (const deviceId of mine) open.push(...await this.deps.summaries.listOpen(deviceId));
|
|
87002
|
-
for (const summary of open) {
|
|
87003
|
-
const born = await this.splitIfApart(summary);
|
|
87004
|
-
await this.sealIfDone(summary);
|
|
87005
|
-
if (born !== null) await this.sealIfDone(born);
|
|
87006
|
-
}
|
|
87007
|
-
} catch (err) {
|
|
87008
|
-
this.deps.logger.warn("summary sweep: could not list open summaries", { meta: { error: errMsg(err) } });
|
|
87009
|
-
} finally {
|
|
87010
|
-
this.sweepInFlight = false;
|
|
87011
|
-
}
|
|
87012
|
-
}
|
|
87013
|
-
/**
|
|
87014
|
-
* Split an open group whose members have walked apart and STAYED apart.
|
|
87015
|
-
*
|
|
87016
|
-
* Returns the last group born, so the sweep can consider it for sealing in
|
|
87017
|
-
* the same pass — a group that divided as everybody left would otherwise sit
|
|
87018
|
-
* open until the next tick with nothing alive in it.
|
|
87019
|
-
*
|
|
87020
|
-
* Every failure here is swallowed into a log: a split is an improvement to a
|
|
87021
|
-
* record that is already correct about WHO was present, and losing it is
|
|
87022
|
-
* never worth losing the seal that follows.
|
|
87023
|
-
*/
|
|
87024
|
-
async splitIfApart(summary) {
|
|
87025
|
-
const { id: summaryId, deviceId } = summary;
|
|
87026
|
-
try {
|
|
87027
|
-
const frame = this.deps.frameSize(deviceId);
|
|
87028
|
-
if (frame === null) {
|
|
87029
|
-
this.apartSince.delete(summaryId);
|
|
87030
|
-
return null;
|
|
87031
|
-
}
|
|
87032
|
-
const members = await this.deps.summaries.membersOf(summaryId);
|
|
87033
|
-
if (members.length < 2) {
|
|
87034
|
-
this.apartSince.delete(summaryId);
|
|
87035
|
-
return null;
|
|
87036
|
-
}
|
|
87037
|
-
const positions = await this.deps.latestPositions(members.map((m) => m.trackId));
|
|
87038
|
-
const placed = [];
|
|
87039
|
-
for (const m of members) {
|
|
87040
|
-
const position = positions.get(m.trackId);
|
|
87041
|
-
if (position !== void 0) placed.push({
|
|
87042
|
-
trackId: m.trackId,
|
|
87043
|
-
position
|
|
87044
|
-
});
|
|
87045
|
-
}
|
|
87046
|
-
const plan = planSplit({
|
|
87047
|
-
placed,
|
|
87048
|
-
primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null
|
|
87049
|
-
}, frame, this.deps.togetherFraction(deviceId));
|
|
87050
|
-
if (plan === null) {
|
|
87051
|
-
this.apartSince.delete(summaryId);
|
|
87052
|
-
return null;
|
|
87053
|
-
}
|
|
87054
|
-
const firstSeen = this.apartSince.get(summaryId);
|
|
87055
|
-
const now = this.deps.now();
|
|
87056
|
-
if (firstSeen === void 0) {
|
|
87057
|
-
this.apartSince.set(summaryId, now);
|
|
87058
|
-
return null;
|
|
87059
|
-
}
|
|
87060
|
-
if (now - firstSeen < 0) return null;
|
|
87061
|
-
let last = null;
|
|
87062
|
-
let row = summary;
|
|
87063
|
-
for (const departing of plan.departures) {
|
|
87064
|
-
const born = await this.deps.summaries.splitOff(row, departing, now);
|
|
87065
|
-
if (born === null) continue;
|
|
87066
|
-
this.moveMirrorMembers(deviceId, summaryId, born.id, departing);
|
|
87067
|
-
row = {
|
|
87068
|
-
...row,
|
|
87069
|
-
memberCount: row.memberCount - departing.length
|
|
87070
|
-
};
|
|
87071
|
-
last = born;
|
|
87072
|
-
}
|
|
87073
|
-
this.apartSince.delete(summaryId);
|
|
87074
|
-
return last;
|
|
87075
|
-
} catch (err) {
|
|
87076
|
-
this.deps.logger.warn("summary: could not divide a group that walked apart", {
|
|
87077
|
-
tags: { deviceId },
|
|
87078
|
-
meta: {
|
|
87079
|
-
summaryId,
|
|
87080
|
-
error: errMsg(err)
|
|
87081
|
-
}
|
|
87082
|
-
});
|
|
87083
|
-
return null;
|
|
87084
|
-
}
|
|
87085
|
-
}
|
|
87086
|
-
/**
|
|
87087
|
-
* Is this group finished?
|
|
87088
|
-
*
|
|
87089
|
-
* Both questions are about THIS group's members, not about the camera. A
|
|
87090
|
-
* camera holds as many groups as it has (D489), so a camera-wide gate means
|
|
87091
|
-
* no group seals until every subject on the camera has gone — and on a busy
|
|
87092
|
-
* camera they never all go at once. Measured: eight summaries open on one
|
|
87093
|
-
* camera over seventeen minutes, none sealed, each new subject opening a
|
|
87094
|
-
* ninth because a group whose members have closed has no positions left for
|
|
87095
|
-
* the proximity rule to judge.
|
|
87096
|
-
*
|
|
87097
|
-
* `null` means "could not find out", and the caller treats that as NOT done —
|
|
87098
|
-
* a read that failed must never seal a live session (D49).
|
|
87099
|
-
*/
|
|
87100
|
-
async isGroupFinished(summary, members) {
|
|
87101
|
-
const { id: summaryId, deviceId } = summary;
|
|
87102
|
-
let openTracks;
|
|
87103
|
-
try {
|
|
87104
|
-
openTracks = await this.deps.openTrackIds(deviceId);
|
|
87105
|
-
} catch (err) {
|
|
87106
|
-
this.deps.logger.warn("summary sweep: could not read open tracks — seal aborted this sweep", {
|
|
87107
|
-
tags: { deviceId },
|
|
87108
|
-
meta: {
|
|
87109
|
-
summaryId,
|
|
87110
|
-
error: errMsg(err)
|
|
87111
|
-
}
|
|
87112
|
-
});
|
|
87113
|
-
return null;
|
|
87114
|
-
}
|
|
87115
|
-
const stillOpen = new Set(openTracks);
|
|
87116
|
-
if (members.some((m) => stillOpen.has(m))) return false;
|
|
87117
|
-
let parked;
|
|
87118
|
-
try {
|
|
87119
|
-
parked = await this.deps.parkedTrackIds(deviceId);
|
|
87120
|
-
} catch (err) {
|
|
87121
|
-
this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
|
|
87122
|
-
tags: { deviceId },
|
|
87123
|
-
meta: {
|
|
87124
|
-
summaryId,
|
|
87125
|
-
error: errMsg(err)
|
|
87126
|
-
}
|
|
87127
|
-
});
|
|
87128
|
-
return null;
|
|
87129
|
-
}
|
|
87130
|
-
return !members.some((m) => parked.has(m));
|
|
87131
|
-
}
|
|
87132
|
-
async sealIfDone(summary) {
|
|
87133
|
-
const { id: summaryId, deviceId } = summary;
|
|
87134
|
-
let members;
|
|
87135
|
-
try {
|
|
87136
|
-
members = (await this.deps.summaries.membersOf(summaryId)).map((m) => m.trackId);
|
|
87137
|
-
} catch (err) {
|
|
87138
|
-
this.deps.logger.warn("summary sweep: could not read members — seal aborted this sweep", {
|
|
87139
|
-
tags: { deviceId },
|
|
87140
|
-
meta: {
|
|
87141
|
-
summaryId,
|
|
87142
|
-
error: errMsg(err)
|
|
87143
|
-
}
|
|
87144
|
-
});
|
|
87145
|
-
return;
|
|
87146
|
-
}
|
|
87147
|
-
if (await this.isGroupFinished(summary, members) !== true) return;
|
|
87148
|
-
const sealedAt = this.deps.now();
|
|
87149
|
-
try {
|
|
87150
|
-
if (await this.deps.summaries.seal(summary, sealedAt)) {
|
|
87151
|
-
const members = this.forgetGroup(deviceId, summaryId);
|
|
87152
|
-
this.deps.onSealed?.(summaryId, members);
|
|
87153
|
-
this.deps.logger.info("summary sealed", {
|
|
87154
|
-
tags: { deviceId },
|
|
87155
|
-
meta: {
|
|
87156
|
-
summaryId,
|
|
87157
|
-
sealedAt
|
|
87158
|
-
}
|
|
87159
|
-
});
|
|
87160
|
-
}
|
|
87161
|
-
} catch (err) {
|
|
87162
|
-
this.deps.logger.warn("summary sweep: seal failed — the row stays open for the next sweep", {
|
|
87163
|
-
tags: { deviceId },
|
|
87164
|
-
meta: {
|
|
87165
|
-
summaryId,
|
|
87166
|
-
error: errMsg(err)
|
|
87167
|
-
}
|
|
87168
|
-
});
|
|
87169
|
-
}
|
|
87170
|
-
}
|
|
87171
|
-
rememberMember(deviceId, summaryIdValue, trackId) {
|
|
87172
|
-
let groups = this.openMirror.get(deviceId);
|
|
87173
|
-
if (groups === void 0) {
|
|
87174
|
-
groups = /* @__PURE__ */ new Map();
|
|
87175
|
-
this.openMirror.set(deviceId, groups);
|
|
87176
|
-
}
|
|
87177
|
-
const members = groups.get(summaryIdValue);
|
|
87178
|
-
if (members === void 0) groups.set(summaryIdValue, new Set([trackId]));
|
|
87179
|
-
else members.add(trackId);
|
|
87180
|
-
}
|
|
87181
|
-
/** Follows a split in the mirror, so the frame path scores each half on its
|
|
87182
|
-
* own members from the very next frame. */
|
|
87183
|
-
moveMirrorMembers(deviceId, fromId, toId, trackIds) {
|
|
87184
|
-
const groups = this.openMirror.get(deviceId);
|
|
87185
|
-
const from = groups?.get(fromId);
|
|
87186
|
-
for (const trackId of trackIds) {
|
|
87187
|
-
from?.delete(trackId);
|
|
87188
|
-
this.rememberMember(deviceId, toId, trackId);
|
|
87189
|
-
}
|
|
87190
|
-
if (from !== void 0 && from.size === 0) groups?.delete(fromId);
|
|
87191
|
-
}
|
|
87192
|
-
/** Drops one group from the mirror and returns the members it held. */
|
|
87193
|
-
forgetGroup(deviceId, summaryIdValue) {
|
|
87194
|
-
this.apartSince.delete(summaryIdValue);
|
|
87195
|
-
const groups = this.openMirror.get(deviceId);
|
|
87196
|
-
const members = groups?.get(summaryIdValue) ?? /* @__PURE__ */ new Set();
|
|
87197
|
-
groups?.delete(summaryIdValue);
|
|
87198
|
-
if (groups !== void 0 && groups.size === 0) this.openMirror.delete(deviceId);
|
|
87199
|
-
return members;
|
|
87200
|
-
}
|
|
87201
|
-
};
|
|
87202
|
-
//#endregion
|
|
87203
|
-
//#region src/pipeline-analytics/summary-settings.ts
|
|
87204
|
-
/**
|
|
87205
|
-
* Per-device summary (group) settings. Cascade: a per-device override on top of
|
|
87206
|
-
* the global default, resolved per field — an invalid or missing value falls
|
|
87207
|
-
* back to its default and parsing never throws. Mirrors `stationary-settings` /
|
|
87208
|
-
* `media-settings`.
|
|
87209
|
-
*
|
|
87210
|
-
* The default IS the constant the proximity rule shipped with, imported and not
|
|
87211
|
-
* copied, so an unset value and a reset value both resolve to exactly today's
|
|
87212
|
-
* behaviour. It is per camera because a threshold belongs to whoever generates
|
|
87213
|
-
* the event: a gate camera framing three metres of path and a drive camera
|
|
87214
|
-
* framing twenty disagree about what "walking together" looks like in a
|
|
87215
|
-
* fraction of the frame, and the fleet measurement behind the default is a
|
|
87216
|
-
* median across both kinds.
|
|
87217
|
-
*/
|
|
87218
|
-
var SummarySettingsSchema = object({
|
|
87219
|
-
/**
|
|
87220
|
-
* How close two subjects must be to count as one group, as a fraction of the
|
|
87221
|
-
* frame. The floor is not zero: zero means "only at the identical pixel",
|
|
87222
|
-
* which ends grouping on that camera while looking like a tuning choice.
|
|
87223
|
-
* See `summary/summary-proximity.ts` for where the default came from.
|
|
87224
|
-
*/
|
|
87225
|
-
summaryTogetherFraction: number().min(.02).max(1).default(DEFAULT_TOGETHER_FRACTION) });
|
|
87226
|
-
var SUMMARY_DEFAULTS = { togetherFraction: SummarySettingsSchema.parse({}).summaryTogetherFraction };
|
|
87227
|
-
function resolveSummarySettings(raw) {
|
|
87228
|
-
const parsed = SummarySettingsSchema.shape.summaryTogetherFraction.safeParse(raw["summaryTogetherFraction"]);
|
|
87229
|
-
return { togetherFraction: parsed.success ? parsed.data : SUMMARY_DEFAULTS.togetherFraction };
|
|
87230
|
-
}
|
|
87231
|
-
//#endregion
|
|
87232
85266
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
87233
85267
|
/**
|
|
87234
85268
|
* Sweep every device with persisted tracks: skip retention-disabled devices,
|
|
@@ -88234,8 +86268,6 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
|
88234
86268
|
* the others) — a longer drain would be killed mid-way and log nothing.
|
|
88235
86269
|
*/
|
|
88236
86270
|
var SENSOR_HANDLER_DRAIN_MS = 2700;
|
|
88237
|
-
/** Throttle for the group-shot gate's window line (D470). */
|
|
88238
|
-
var GROUP_SHOT_METRIC_INTERVAL_MS = 3e4;
|
|
88239
86271
|
/**
|
|
88240
86272
|
* How often the addon asks "is a gallery sample outside the cluster face
|
|
88241
86273
|
* space?". A flip is rare and the check is two cheap reads — a minute keeps
|
|
@@ -88513,21 +86545,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
88513
86545
|
storageMigrationLeaseId = null;
|
|
88514
86546
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
88515
86547
|
sensorEventStore = null;
|
|
88516
|
-
/** The per-camera session envelope (D359). Bound with the other stores at boot. */
|
|
88517
|
-
summaryStore = null;
|
|
88518
|
-
/** The ONLY writer of a summary's lifecycle (D359). */
|
|
88519
|
-
summarySealer = null;
|
|
88520
|
-
/** The materialisation gate on every open summary's group shot (D359 phase 3, D470). RAM only. */
|
|
88521
|
-
groupShots = new GroupShotTracker();
|
|
88522
|
-
/** Throttle for the group-shot gate's window line (D470). */
|
|
88523
|
-
lastGroupShotMetricAt = 0;
|
|
88524
|
-
/**
|
|
88525
|
-
* Tracks a gallery match has named, for the group-shot score's recognition
|
|
88526
|
-
* term. "Recognised so far" rather than "at that instant": the match arrives
|
|
88527
|
-
* on the detail plane, after the frame; the frames that follow carry it.
|
|
88528
|
-
* Pruned when the track's summary seals, so it is bounded by open summaries.
|
|
88529
|
-
*/
|
|
88530
|
-
recognizedTrackIds = /* @__PURE__ */ new Set();
|
|
88531
86548
|
/** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
|
|
88532
86549
|
* so the DeviceStateChanged handler stays cheap. */
|
|
88533
86550
|
linkedCamerasCache = null;
|
|
@@ -88808,7 +86825,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
88808
86825
|
};
|
|
88809
86826
|
this.ctx.logger.info("nc-action data-plane served", { meta: { served: served !== null } });
|
|
88810
86827
|
} catch (err) {
|
|
88811
|
-
this.ctx.logger.warn("nc-action data-plane failed to serve", { meta: { error: errMsg
|
|
86828
|
+
this.ctx.logger.warn("nc-action data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
88812
86829
|
}
|
|
88813
86830
|
}
|
|
88814
86831
|
/** Mints a button's callback URL once the action plane is served; null until
|
|
@@ -88842,7 +86859,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
88842
86859
|
await store.sweep();
|
|
88843
86860
|
await index.prune(this.isPostProcessingNode ? await store.keptIds() : null);
|
|
88844
86861
|
} catch (err) {
|
|
88845
|
-
this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg
|
|
86862
|
+
this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg(err) } });
|
|
88846
86863
|
}
|
|
88847
86864
|
};
|
|
88848
86865
|
run();
|
|
@@ -88912,7 +86929,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
88912
86929
|
retentionDays: store.retentionMs === null ? "forever" : Math.round(store.retentionMs / 864e5)
|
|
88913
86930
|
} });
|
|
88914
86931
|
} catch (err) {
|
|
88915
|
-
this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg
|
|
86932
|
+
this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
88916
86933
|
}
|
|
88917
86934
|
}
|
|
88918
86935
|
/** Public base URL for event thumbnails: `/addon/<addonId>/event-media`.
|
|
@@ -89049,11 +87066,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89049
87066
|
* the partition + promotion + sweep call sites — an operator change takes
|
|
89050
87067
|
* effect within one SETTINGS_CACHE_TTL_MS tick without an addon restart. */
|
|
89051
87068
|
stationarySettingsCache = this.settingsCaches.registerDeviceCache("stationary", SETTINGS_CACHE_TTL_MS);
|
|
89052
|
-
/** Per-device summary (group) settings — the proximity threshold. Read
|
|
89053
|
-
* SYNCHRONOUSLY by the sealer's birth and split paths, so it is warmed on
|
|
89054
|
-
* the frame path like the stationary one and a cold miss answers the
|
|
89055
|
-
* measured default. */
|
|
89056
|
-
summarySettingsCache = this.settingsCaches.registerDeviceCache("summary", SETTINGS_CACHE_TTL_MS);
|
|
89057
87069
|
/** Per-device STILLNESS SCALE cache — the measured "how slow is settled" bar
|
|
89058
87070
|
* (`stillness-settings.ts`). Read once per FrameProcessor build, so this is
|
|
89059
87071
|
* a cold-path cache; it exists so a calibration applied through
|
|
@@ -89183,9 +87195,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89183
87195
|
* line/min/device instead of one per §5 cadence tick, see media-capture-log.ts). */
|
|
89184
87196
|
mediaCaptureLog = new MediaCaptureLogAggregator();
|
|
89185
87197
|
/** Per-device hit/miss window for the native `keyFrame` path (2026-08-06).
|
|
89186
|
-
* Before it there was NO metric here at all —
|
|
89187
|
-
*
|
|
89188
|
-
*
|
|
87198
|
+
* Before it there was NO metric here at all — the by-handle key-frame fetch
|
|
87199
|
+
* in `runtime/native-frame-transport.ts` (retired by D422) never touched
|
|
87200
|
+
* `bumpCropMetric` — so whether a key-frame fix worked could only be
|
|
87201
|
+
* answered by hand-counting Loki lines. See `key-frame-metrics.ts`. */
|
|
89189
87202
|
keyFrameCaptureLog = new KeyFrameCaptureLog();
|
|
89190
87203
|
/** The registry's own liveness answer, mirrored off `ClusterTopologySnapshot`,
|
|
89191
87204
|
* so a capture is never dispatched at a node the hub already called dead.
|
|
@@ -89215,7 +87228,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89215
87228
|
tags: { deviceId },
|
|
89216
87229
|
meta: {
|
|
89217
87230
|
deviceId,
|
|
89218
|
-
error: errMsg
|
|
87231
|
+
error: errMsg(error)
|
|
89219
87232
|
}
|
|
89220
87233
|
});
|
|
89221
87234
|
}
|
|
@@ -89244,7 +87257,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89244
87257
|
tags: { deviceId },
|
|
89245
87258
|
meta: {
|
|
89246
87259
|
deviceId,
|
|
89247
|
-
error: errMsg
|
|
87260
|
+
error: errMsg(error)
|
|
89248
87261
|
}
|
|
89249
87262
|
});
|
|
89250
87263
|
}
|
|
@@ -89319,12 +87332,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89319
87332
|
});
|
|
89320
87333
|
}
|
|
89321
87334
|
captureCrop = null;
|
|
89322
|
-
/** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
|
|
89323
|
-
* to the owning runner, PLUS the source `tier`. Captured in the async init
|
|
89324
|
-
* block so `persistKeyFrames` can fetch the native full frame for the
|
|
89325
|
-
* `keyFrame` + `keyFrameSmall` writes in the same live-frame window AND reject
|
|
89326
|
-
* a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
|
|
89327
|
-
getNativeKeyFrameRgb = null;
|
|
89328
87335
|
shuttingDown = false;
|
|
89329
87336
|
/**
|
|
89330
87337
|
* Sensor handlers the bus dispatched under `void` that have not finished.
|
|
@@ -89824,7 +87831,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89824
87831
|
logger,
|
|
89825
87832
|
noteCropOutcome: (deviceId, reason) => this.failureReport.noteEnrichmentCrop(deviceId, reason)
|
|
89826
87833
|
});
|
|
89827
|
-
this.getNativeKeyFrameRgb = transport.getNativeKeyFrameRgb;
|
|
89828
87834
|
this.parkedFrames = createParkedFrameClient({
|
|
89829
87835
|
api: api.pipelineRunner,
|
|
89830
87836
|
logger: logger.child("ParkedFrames"),
|
|
@@ -90037,7 +88043,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90037
88043
|
*/
|
|
90038
88044
|
armAlarmPanelDeclaration(api) {
|
|
90039
88045
|
this.declareAlarmPanel(api).catch((err) => {
|
|
90040
|
-
this.ctx.logger.warn("alarm panel declaration could not be armed — the Alarm tab stays empty until this addon restarts", { meta: { error: errMsg
|
|
88046
|
+
this.ctx.logger.warn("alarm panel declaration could not be armed — the Alarm tab stays empty until this addon restarts", { meta: { error: errMsg(err) } });
|
|
90041
88047
|
});
|
|
90042
88048
|
}
|
|
90043
88049
|
async declareAlarmPanel(api) {
|
|
@@ -90161,7 +88167,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90161
88167
|
MediaStore.declare(api.settingsStore),
|
|
90162
88168
|
EventStore.declare(api.settingsStore),
|
|
90163
88169
|
SensorEventStore.declare(api.settingsStore),
|
|
90164
|
-
SummaryStore.declare(api.settingsStore),
|
|
90165
88170
|
IdentityStore.declare(api.settingsStore),
|
|
90166
88171
|
FaceStore.declare(api.settingsStore),
|
|
90167
88172
|
PlateStore.declare(api.settingsStore),
|
|
@@ -90190,7 +88195,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90190
88195
|
logger: this.ctx.logger.child("viewer-settings")
|
|
90191
88196
|
});
|
|
90192
88197
|
} catch (err) {
|
|
90193
|
-
this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg
|
|
88198
|
+
this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg(err) } });
|
|
90194
88199
|
}
|
|
90195
88200
|
runLabelTierMigration({
|
|
90196
88201
|
store: api.settingsStore,
|
|
@@ -90288,7 +88293,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90288
88293
|
try {
|
|
90289
88294
|
locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
|
|
90290
88295
|
} catch (err) {
|
|
90291
|
-
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg
|
|
88296
|
+
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg(err) } });
|
|
90292
88297
|
this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
|
|
90293
88298
|
return DEFAULT_MEDIA_LOCATION;
|
|
90294
88299
|
}
|
|
@@ -90469,49 +88474,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90469
88474
|
store: api.settingsStore,
|
|
90470
88475
|
logger: logger.child("SensorEventStore")
|
|
90471
88476
|
});
|
|
90472
|
-
this.summaryStore = new SummaryStore({
|
|
90473
|
-
trackIdsMatching: async (terms) => await this.trackStore?.trackIdsMatching(terms) ?? [],
|
|
90474
|
-
store: api.settingsStore,
|
|
90475
|
-
logger: this.ctx.logger.child("summary"),
|
|
90476
|
-
onEvict: async (ids) => {
|
|
90477
|
-
await this.mediaStore?.deleteForOwner("summary", ids);
|
|
90478
|
-
}
|
|
90479
|
-
});
|
|
90480
|
-
this.summarySealer = new SummarySealer({
|
|
90481
|
-
summaries: this.summaryStore,
|
|
90482
|
-
analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
|
|
90483
|
-
openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
|
|
90484
|
-
parkedTrackIds: async (deviceId) => {
|
|
90485
|
-
const ids = /* @__PURE__ */ new Set();
|
|
90486
|
-
for (const entry of this.stationaryRegistry?.list(deviceId) ?? []) if (entry.sourceTrackId !== void 0) ids.add(entry.sourceTrackId);
|
|
90487
|
-
return ids;
|
|
90488
|
-
},
|
|
90489
|
-
now: () => Date.now(),
|
|
90490
|
-
logger: this.ctx.logger.child("summary-sealer"),
|
|
90491
|
-
frameSize: (deviceId) => {
|
|
90492
|
-
const d = this.lastFrameDimsByDevice.get(deviceId);
|
|
90493
|
-
return d === void 0 ? null : {
|
|
90494
|
-
width: d.w,
|
|
90495
|
-
height: d.h
|
|
90496
|
-
};
|
|
90497
|
-
},
|
|
90498
|
-
latestPositions: async (trackIds) => {
|
|
90499
|
-
const out = /* @__PURE__ */ new Map();
|
|
90500
|
-
for (const id of trackIds) {
|
|
90501
|
-
const last = (this.trackStore?.peekActive(id))?.positions.at(-1);
|
|
90502
|
-
if (last !== void 0) out.set(id, {
|
|
90503
|
-
x: last.x,
|
|
90504
|
-
y: last.y
|
|
90505
|
-
});
|
|
90506
|
-
}
|
|
90507
|
-
return out;
|
|
90508
|
-
},
|
|
90509
|
-
togetherFraction: (deviceId) => this.summarySettingsFromCache(deviceId).togetherFraction,
|
|
90510
|
-
onSealed: (summaryId, members) => {
|
|
90511
|
-
this.groupShots.forget(summaryId);
|
|
90512
|
-
for (const trackId of members) this.recognizedTrackIds.delete(trackId);
|
|
90513
|
-
}
|
|
90514
|
-
});
|
|
90515
88477
|
this.packageDropDetector = new PackageDropDetector({
|
|
90516
88478
|
events: eventStore,
|
|
90517
88479
|
emit: {
|
|
@@ -90549,7 +88511,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90549
88511
|
onPersisted: (ev, phase) => this.notificationCenter?.onPackageEventPersisted(ev, phase),
|
|
90550
88512
|
onError: (scope, err) => logger.warn("package-drop detector error", { meta: {
|
|
90551
88513
|
scope,
|
|
90552
|
-
error: errMsg
|
|
88514
|
+
error: errMsg(err)
|
|
90553
88515
|
} })
|
|
90554
88516
|
});
|
|
90555
88517
|
this.linkedCamerasCache = new LinkedCamerasCache({
|
|
@@ -90565,7 +88527,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90565
88527
|
...cameraId !== void 0 ? { tags: { deviceId: cameraId } } : {},
|
|
90566
88528
|
meta: {
|
|
90567
88529
|
scope,
|
|
90568
|
-
error: errMsg
|
|
88530
|
+
error: errMsg(err)
|
|
90569
88531
|
}
|
|
90570
88532
|
})
|
|
90571
88533
|
});
|
|
@@ -90577,7 +88539,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90577
88539
|
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
90578
88540
|
meta: {
|
|
90579
88541
|
scope,
|
|
90580
|
-
error: errMsg
|
|
88542
|
+
error: errMsg(err)
|
|
90581
88543
|
}
|
|
90582
88544
|
})
|
|
90583
88545
|
});
|
|
@@ -90590,7 +88552,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90590
88552
|
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
90591
88553
|
meta: {
|
|
90592
88554
|
scope,
|
|
90593
|
-
error: errMsg
|
|
88555
|
+
error: errMsg(err)
|
|
90594
88556
|
}
|
|
90595
88557
|
})
|
|
90596
88558
|
});
|
|
@@ -90600,7 +88562,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90600
88562
|
logger: logger.child("AudioMarkers"),
|
|
90601
88563
|
onError: (scope, err) => logger.warn("audio marker error", { meta: {
|
|
90602
88564
|
scope,
|
|
90603
|
-
error: errMsg
|
|
88565
|
+
error: errMsg(err)
|
|
90604
88566
|
} })
|
|
90605
88567
|
});
|
|
90606
88568
|
const galleryVectorStore = api.vectorStore;
|
|
@@ -90711,7 +88673,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90711
88673
|
score: face.matchScore ?? face.confidence,
|
|
90712
88674
|
confidence: face.confidence,
|
|
90713
88675
|
...face.frameHandle !== void 0 ? { frameHandle: face.frameHandle } : {}
|
|
90714
|
-
}
|
|
88676
|
+
}),
|
|
90715
88677
|
onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
|
|
90716
88678
|
faceSourceFrames: new FaceSourceFrameKeeper({
|
|
90717
88679
|
port: createFaceSourceParkPort({
|
|
@@ -90957,7 +88919,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
90957
88919
|
} catch (err) {
|
|
90958
88920
|
this.ctx.logger.debug("zone ownership read failed", {
|
|
90959
88921
|
tags: { deviceId },
|
|
90960
|
-
meta: { error: errMsg
|
|
88922
|
+
meta: { error: errMsg(err) }
|
|
90961
88923
|
});
|
|
90962
88924
|
}
|
|
90963
88925
|
return out;
|
|
@@ -91017,7 +88979,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91017
88979
|
} catch (err) {
|
|
91018
88980
|
return {
|
|
91019
88981
|
ok: false,
|
|
91020
|
-
error: errMsg
|
|
88982
|
+
error: errMsg(err)
|
|
91021
88983
|
};
|
|
91022
88984
|
}
|
|
91023
88985
|
},
|
|
@@ -91037,7 +88999,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91037
88999
|
} catch (err) {
|
|
91038
89000
|
return {
|
|
91039
89001
|
ok: false,
|
|
91040
|
-
error: errMsg
|
|
89002
|
+
error: errMsg(err)
|
|
91041
89003
|
};
|
|
91042
89004
|
}
|
|
91043
89005
|
},
|
|
@@ -91239,30 +89201,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91239
89201
|
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
91240
89202
|
}));
|
|
91241
89203
|
},
|
|
91242
|
-
listSummaryGroups: async ({ deviceId, sinceMs, untilMs }) => {
|
|
91243
|
-
const summaryStore = this.summaryStore;
|
|
91244
|
-
if (!summaryStore) return [];
|
|
91245
|
-
const rows = await summaryStore.listRecent({
|
|
91246
|
-
deviceIds: [deviceId],
|
|
91247
|
-
since: sinceMs,
|
|
91248
|
-
until: untilMs,
|
|
91249
|
-
limit: 500
|
|
91250
|
-
});
|
|
91251
|
-
const out = [];
|
|
91252
|
-
for (const row of rows) {
|
|
91253
|
-
const members = await summaryStore.membersOf(row.id);
|
|
91254
|
-
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|
|
91255
|
-
const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
|
|
91256
|
-
out.push({
|
|
91257
|
-
summaryId: row.id,
|
|
91258
|
-
deviceId: row.deviceId,
|
|
91259
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
91260
|
-
primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null,
|
|
91261
|
-
groupShotMediaId: shot?.mediaId ?? null
|
|
91262
|
-
});
|
|
91263
|
-
}
|
|
91264
|
-
return out;
|
|
91265
|
-
},
|
|
91266
89204
|
listMediaInfo: async (id) => {
|
|
91267
89205
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
91268
89206
|
mediaId: r.mediaId,
|
|
@@ -91489,7 +89427,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91489
89427
|
} catch (err) {
|
|
91490
89428
|
this.ctx.logger.warn("event-media resolve failed", { meta: {
|
|
91491
89429
|
address,
|
|
91492
|
-
error: errMsg
|
|
89430
|
+
error: errMsg(err)
|
|
91493
89431
|
} });
|
|
91494
89432
|
throw err;
|
|
91495
89433
|
}
|
|
@@ -91510,7 +89448,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91510
89448
|
access: planeAccess("event-media")
|
|
91511
89449
|
} });
|
|
91512
89450
|
} catch (err) {
|
|
91513
|
-
this.ctx.logger.warn("event-media data-plane failed to serve", { meta: { error: errMsg
|
|
89451
|
+
this.ctx.logger.warn("event-media data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
91514
89452
|
}
|
|
91515
89453
|
}
|
|
91516
89454
|
/**
|
|
@@ -91536,7 +89474,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91536
89474
|
handler
|
|
91537
89475
|
}) ?? null;
|
|
91538
89476
|
} catch (err) {
|
|
91539
|
-
this.ctx.logger.warn("training-export data-plane failed to serve", { meta: { error: errMsg
|
|
89477
|
+
this.ctx.logger.warn("training-export data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
91540
89478
|
}
|
|
91541
89479
|
}
|
|
91542
89480
|
/**
|
|
@@ -91828,7 +89766,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91828
89766
|
handler
|
|
91829
89767
|
}) ?? null;
|
|
91830
89768
|
} catch (err) {
|
|
91831
|
-
this.ctx.logger.warn("debug-export data-plane failed to serve", { meta: { error: errMsg
|
|
89769
|
+
this.ctx.logger.warn("debug-export data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
91832
89770
|
}
|
|
91833
89771
|
}
|
|
91834
89772
|
/**
|
|
@@ -91857,7 +89795,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91857
89795
|
handler
|
|
91858
89796
|
}) ?? null;
|
|
91859
89797
|
} catch (err) {
|
|
91860
|
-
this.ctx.logger.warn("retrain-export data-plane failed to serve", { meta: { error: errMsg
|
|
89798
|
+
this.ctx.logger.warn("retrain-export data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
91861
89799
|
}
|
|
91862
89800
|
}
|
|
91863
89801
|
/**
|
|
@@ -91922,7 +89860,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
91922
89860
|
tags: { deviceId },
|
|
91923
89861
|
meta: {
|
|
91924
89862
|
deviceId,
|
|
91925
|
-
error: errMsg
|
|
89863
|
+
error: errMsg(err)
|
|
91926
89864
|
}
|
|
91927
89865
|
});
|
|
91928
89866
|
return null;
|
|
@@ -92041,7 +89979,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
92041
89979
|
}, RETENTION_SWEEP_INTERVAL_MS);
|
|
92042
89980
|
this.ltsFlushTimer = setInterval(() => {
|
|
92043
89981
|
this.lts?.flushDue().catch((err) => {
|
|
92044
|
-
this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg
|
|
89982
|
+
this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg(err) } });
|
|
92045
89983
|
});
|
|
92046
89984
|
}, LTS_FLUSH_INTERVAL_MS);
|
|
92047
89985
|
this.startOrphanAuditTimer();
|
|
@@ -92062,7 +90000,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
92062
90000
|
} catch (err) {
|
|
92063
90001
|
this.ctx.logger.error("artifacts storage location is NOT available — enrolling a face, a plate or a scene reference will FAIL until it is", { meta: {
|
|
92064
90002
|
location: ARTIFACTS_MEDIA_LOCATION,
|
|
92065
|
-
error: errMsg
|
|
90003
|
+
error: errMsg(err),
|
|
92066
90004
|
hint: "the declaration is seeded when a storage-provider registers; restart this node so the manifest is re-read, or create an artifacts location. Refusing the write is deliberate — the footage volume is where the last gallery was lost (D197), and the addon data dir carries no media (D341)."
|
|
92067
90005
|
} });
|
|
92068
90006
|
return;
|
|
@@ -92079,7 +90017,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
92079
90017
|
hint: "the pixels are gone for good; re-enrol these people from a photo"
|
|
92080
90018
|
} });
|
|
92081
90019
|
} catch (err) {
|
|
92082
|
-
this.ctx.logger.warn("enrolled gallery media reconcile failed", { meta: { error: errMsg
|
|
90020
|
+
this.ctx.logger.warn("enrolled gallery media reconcile failed", { meta: { error: errMsg(err) } });
|
|
92083
90021
|
}
|
|
92084
90022
|
try {
|
|
92085
90023
|
const identityStore = this.identityStore;
|
|
@@ -92111,7 +90049,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
92111
90049
|
maxReclaimPerRun: 16
|
|
92112
90050
|
})).reclaimed > 0) await this.faceRecognizer?.refreshGallery();
|
|
92113
90051
|
} catch (err) {
|
|
92114
|
-
this.ctx.logger.warn("gallery sample reconcile failed", { meta: { error: errMsg
|
|
90052
|
+
this.ctx.logger.warn("gallery sample reconcile failed", { meta: { error: errMsg(err) } });
|
|
92115
90053
|
}
|
|
92116
90054
|
}
|
|
92117
90055
|
/** Schedule the orphan audit on the operator's interval (hours). */
|
|
@@ -92253,7 +90191,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
92253
90191
|
engine.start();
|
|
92254
90192
|
this.sceneEngine = engine;
|
|
92255
90193
|
this.sceneReloadTimer = setInterval(() => {
|
|
92256
|
-
store.load().then(() => store.pruneLatches()).catch((err) => log.debug("scene reload failed", { meta: { error: errMsg
|
|
90194
|
+
store.load().then(() => store.pruneLatches()).catch((err) => log.debug("scene reload failed", { meta: { error: errMsg(err) } }));
|
|
92257
90195
|
}, SCENE_RELOAD_INTERVAL_MS);
|
|
92258
90196
|
this.sceneReloadTimer.unref?.();
|
|
92259
90197
|
}
|
|
@@ -92297,7 +90235,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
92297
90235
|
lng: status.location.longitude
|
|
92298
90236
|
};
|
|
92299
90237
|
} catch (err) {
|
|
92300
|
-
this.ctx.logger.debug("site location read failed", { meta: { error: errMsg
|
|
90238
|
+
this.ctx.logger.debug("site location read failed", { meta: { error: errMsg(err) } });
|
|
92301
90239
|
}
|
|
92302
90240
|
this.siteCoordsCache = {
|
|
92303
90241
|
value,
|
|
@@ -92316,7 +90254,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
92316
90254
|
} catch (err) {
|
|
92317
90255
|
this.ctx.logger.debug("scene slice mirror failed", {
|
|
92318
90256
|
tags: { deviceId },
|
|
92319
|
-
meta: { error: errMsg
|
|
90257
|
+
meta: { error: errMsg(err) }
|
|
92320
90258
|
});
|
|
92321
90259
|
}
|
|
92322
90260
|
}
|
|
@@ -92686,7 +90624,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
92686
90624
|
if (planeRunsStationary(source) && this.stationaryRegistry) {
|
|
92687
90625
|
await this.resolveDeviceStationarySettings(deviceId);
|
|
92688
90626
|
await this.resolveDevicePackageDropSettings(deviceId);
|
|
92689
|
-
await this.resolveDeviceSummarySettings(deviceId);
|
|
92690
90627
|
}
|
|
92691
90628
|
const processor = await this.getOrCreateProcessor(deviceId, source);
|
|
92692
90629
|
const proxy = await this.ensureProxy(deviceId);
|
|
@@ -93062,15 +90999,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
93062
90999
|
className: t.className
|
|
93063
91000
|
}
|
|
93064
91001
|
});
|
|
93065
|
-
this.summarySealer?.onTrackOpened({
|
|
93066
|
-
deviceId,
|
|
93067
|
-
trackId: id,
|
|
93068
|
-
className: t.className,
|
|
93069
|
-
position: {
|
|
93070
|
-
x: t.bbox.x + t.bbox.w / 2,
|
|
93071
|
-
y: t.bbox.y + t.bbox.h / 2
|
|
93072
|
-
}
|
|
93073
|
-
});
|
|
93074
91002
|
const startPayload = buildTrackLifecyclePayload({
|
|
93075
91003
|
deviceId,
|
|
93076
91004
|
trackId: id,
|
|
@@ -93306,7 +91234,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
93306
91234
|
for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
|
|
93307
91235
|
else plateCrops += 1;
|
|
93308
91236
|
const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight, frameHandle);
|
|
93309
|
-
this.offerGroupShot(deviceId, result, frameHandle);
|
|
93310
91237
|
for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
|
|
93311
91238
|
const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
|
|
93312
91239
|
const rasterFallbackCandidates = [];
|
|
@@ -93670,7 +91597,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
93670
91597
|
} catch (err) {
|
|
93671
91598
|
this.ctx.logger.debug("confirmation gate: getGlobalSteps failed", { meta: {
|
|
93672
91599
|
nodeId,
|
|
93673
|
-
error: errMsg
|
|
91600
|
+
error: errMsg(err)
|
|
93674
91601
|
} });
|
|
93675
91602
|
}
|
|
93676
91603
|
this.confirmationDetectorStepByNode.set(nodeId, {
|
|
@@ -93714,7 +91641,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
93714
91641
|
} catch (err) {
|
|
93715
91642
|
this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
|
|
93716
91643
|
tags: { deviceId },
|
|
93717
|
-
meta: { error: errMsg
|
|
91644
|
+
meta: { error: errMsg(err) }
|
|
93718
91645
|
});
|
|
93719
91646
|
return null;
|
|
93720
91647
|
}
|
|
@@ -94022,15 +91949,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
94022
91949
|
stationarySettingsFromCache(deviceId) {
|
|
94023
91950
|
return this.stationarySettingsCache.peek(deviceId) ?? STATIONARY_DEFAULTS;
|
|
94024
91951
|
}
|
|
94025
|
-
/** Synchronous read of the per-device summary settings — warmed by
|
|
94026
|
-
* `processFrame`. A cold miss is today's constant, never a zero: a zero
|
|
94027
|
-
* threshold would quietly end grouping on that camera. */
|
|
94028
|
-
summarySettingsFromCache(deviceId) {
|
|
94029
|
-
return this.summarySettingsCache.peek(deviceId) ?? SUMMARY_DEFAULTS;
|
|
94030
|
-
}
|
|
94031
|
-
async resolveDeviceSummarySettings(deviceId) {
|
|
94032
|
-
return this.summarySettingsCache.get(deviceId, (id) => this.readDeviceSettings(id, resolveSummarySettings));
|
|
94033
|
-
}
|
|
94034
91952
|
/** Synchronous read of the per-device package-drop settings — warmed by
|
|
94035
91953
|
* `processFrame` before the promotion loop, exactly like the stationary one. */
|
|
94036
91954
|
packageDropSettingsFromCache(deviceId) {
|
|
@@ -94114,12 +92032,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
94114
92032
|
} } : {}
|
|
94115
92033
|
});
|
|
94116
92034
|
}
|
|
94117
|
-
if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame)
|
|
92035
|
+
if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame));
|
|
94118
92036
|
}
|
|
94119
92037
|
const resolved = d.className === "plate" ? this.plateRecognizer?.resolveLabelWithVehicle(d.label, d.score) ?? null : { text: d.label };
|
|
94120
92038
|
if (resolved !== null && resolved.text !== void 0) {
|
|
94121
92039
|
const applied = await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId);
|
|
94122
|
-
if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame)
|
|
92040
|
+
if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame));
|
|
94123
92041
|
}
|
|
94124
92042
|
} else if (d.className === "plate") this.failureReport.notePlateRead(deviceId, d.bbox === void 0 ? REASON_NO_BBOX : REASON_EMPTY_READ);
|
|
94125
92043
|
} catch (err) {
|
|
@@ -94128,7 +92046,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
94128
92046
|
meta: {
|
|
94129
92047
|
trackId,
|
|
94130
92048
|
stepId: d.stepId,
|
|
94131
|
-
error: errMsg
|
|
92049
|
+
error: errMsg(err)
|
|
94132
92050
|
}
|
|
94133
92051
|
});
|
|
94134
92052
|
}
|
|
@@ -94264,7 +92182,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
94264
92182
|
tags: { deviceId },
|
|
94265
92183
|
meta: {
|
|
94266
92184
|
trackId: t.trackId,
|
|
94267
|
-
error: errMsg
|
|
92185
|
+
error: errMsg(err)
|
|
94268
92186
|
}
|
|
94269
92187
|
});
|
|
94270
92188
|
}
|
|
@@ -94312,139 +92230,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
94312
92230
|
});
|
|
94313
92231
|
}
|
|
94314
92232
|
/**
|
|
94315
|
-
* Capture the CLEAN best full frame per given track at this best-detection
|
|
94316
|
-
* moment and store BOTH variants (`putReplacing` → one row per track, per
|
|
94317
|
-
* kind): the native `keyFrame` (full frame fetched at `KEYFRAME_NATIVE_MAX_WIDTH`
|
|
94318
|
-
* — true native on a 4K cam) plus its 960 companion `keyFrameSmall`, encoded
|
|
94319
|
-
* from the SAME native surface (no second fetch). No box is drawn — these are
|
|
94320
|
-
* the clean hero images (`keyFrameSmall` is the web-friendly one).
|
|
94321
|
-
*
|
|
94322
|
-
* NATIVE-OR-NOTHING: a miss writes nothing (no upscale, no ≤640 stand-in) and
|
|
94323
|
-
* the track's BOUNDED catch-up retry re-fires on a later frame. Until
|
|
94324
|
-
* 2026-08-06 the only re-fire was the next genuine new-best PEAK, which for
|
|
94325
|
-
* 80% of the tracks that missed never came — see `key-frame-retry.ts`. The
|
|
94326
|
-
* `keyFrame` key is recorded on the track's resident state so the face /
|
|
94327
|
-
* plate / object-embedding rows LINK the SAME native key frame (Design B).
|
|
94328
|
-
* Issued in the live-frame window so the native lease is still held.
|
|
94329
|
-
* Best-effort (D8) — a per-track failure is logged and never thrown.
|
|
94330
|
-
*
|
|
94331
|
-
* The capture BODY lives in `pipeline/key-frame-persist.ts`; what stays here
|
|
94332
|
-
* is the scheduling (coalescing, priority, budget) and the bookkeeping the
|
|
94333
|
-
* retry gate reads.
|
|
94334
|
-
*/
|
|
94335
|
-
/**
|
|
94336
|
-
* Offer one processed frame to the camera's OPEN summary (D359 phase 3).
|
|
94337
|
-
* Metadata only, at frame-rate: the frame is scored against the members the
|
|
94338
|
-
* sealer has mirrored, and the pixels are fetched — by handle, at the owning
|
|
94339
|
-
* node, as a native key frame — only when this frame beats the best so far.
|
|
94340
|
-
*/
|
|
94341
|
-
offerGroupShot(deviceId, result, frameHandle) {
|
|
94342
|
-
if (frameHandle === void 0) return;
|
|
94343
|
-
const groups = this.summarySealer?.openGroupsFor(deviceId) ?? [];
|
|
94344
|
-
for (const open of groups) this.offerGroupShotTo(deviceId, open, result, frameHandle);
|
|
94345
|
-
if (groups.length > 0) this.logGroupShotWindow(deviceId);
|
|
94346
|
-
}
|
|
94347
|
-
/** One group's turn at the frame. Split out of {@link offerGroupShot} so the
|
|
94348
|
-
* scoring reads the members of the group it is about, and nothing else. */
|
|
94349
|
-
offerGroupShotTo(deviceId, open, result, frameHandle) {
|
|
94350
|
-
const score = scoreSummaryFrame({
|
|
94351
|
-
members: open.members,
|
|
94352
|
-
frameWidth: result.frameWidth,
|
|
94353
|
-
frameHeight: result.frameHeight,
|
|
94354
|
-
detections: result.tracked.map((t) => ({
|
|
94355
|
-
trackId: t.trackId,
|
|
94356
|
-
bbox: {
|
|
94357
|
-
x: t.bbox.x,
|
|
94358
|
-
y: t.bbox.y,
|
|
94359
|
-
width: t.bbox.w,
|
|
94360
|
-
height: t.bbox.h
|
|
94361
|
-
},
|
|
94362
|
-
confidence: t.confidence,
|
|
94363
|
-
recognized: this.recognizedTrackIds.has(t.trackId)
|
|
94364
|
-
}))
|
|
94365
|
-
});
|
|
94366
|
-
const frameRef = { timestamp: result.timestamp };
|
|
94367
|
-
if (!this.groupShots.offer(open.id, score, frameRef)) return;
|
|
94368
|
-
const getNative = this.getNativeKeyFrameRgb;
|
|
94369
|
-
const mediaStore = this.mediaStore;
|
|
94370
|
-
if (!getNative || !mediaStore) return;
|
|
94371
|
-
const summaryId = open.id;
|
|
94372
|
-
this.captureScheduler.request({
|
|
94373
|
-
deviceId,
|
|
94374
|
-
kind: "keyFrame",
|
|
94375
|
-
coalesceKey: `summary:${summaryId}:keyFrame`,
|
|
94376
|
-
exec: () => persistSummaryGroupShot({
|
|
94377
|
-
fetchNativeFullFrame: getNative,
|
|
94378
|
-
encodeVariants: encodeKeyFrameVariants,
|
|
94379
|
-
media: mediaStore,
|
|
94380
|
-
logger: this.ctx.logger,
|
|
94381
|
-
cropFrame: async (frame, rect) => {
|
|
94382
|
-
const out = await extractCropRaw(frame.data, frame.width, frame.height, {
|
|
94383
|
-
x: rect.x / frame.width,
|
|
94384
|
-
y: rect.y / frame.height,
|
|
94385
|
-
w: rect.width / frame.width,
|
|
94386
|
-
h: rect.height / frame.height
|
|
94387
|
-
});
|
|
94388
|
-
return {
|
|
94389
|
-
...frame,
|
|
94390
|
-
data: out.data,
|
|
94391
|
-
width: out.width,
|
|
94392
|
-
height: out.height
|
|
94393
|
-
};
|
|
94394
|
-
}
|
|
94395
|
-
}, {
|
|
94396
|
-
deviceId,
|
|
94397
|
-
summaryId,
|
|
94398
|
-
timestamp: result.timestamp,
|
|
94399
|
-
frameHandle,
|
|
94400
|
-
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
|
|
94401
|
-
crop: score.crop,
|
|
94402
|
-
cropFrameWidth: result.frameWidth,
|
|
94403
|
-
cropFrameHeight: result.frameHeight
|
|
94404
|
-
})
|
|
94405
|
-
}).then((outcome) => {
|
|
94406
|
-
if (outcome?.kind === "landed") {
|
|
94407
|
-
this.groupShots.noteLanded(summaryId, score, frameRef);
|
|
94408
|
-
this.ctx.logger.debug("summary group shot landed", {
|
|
94409
|
-
tags: { deviceId },
|
|
94410
|
-
meta: {
|
|
94411
|
-
summaryId,
|
|
94412
|
-
visible: score.visible,
|
|
94413
|
-
quality: Number(score.quality.toFixed(3))
|
|
94414
|
-
}
|
|
94415
|
-
});
|
|
94416
|
-
}
|
|
94417
|
-
}).catch(() => {});
|
|
94418
|
-
}
|
|
94419
|
-
/**
|
|
94420
|
-
* The group-shot gate's window, throttled to one line per
|
|
94421
|
-
* {@link GROUP_SHOT_METRIC_INTERVAL_MS}. Process-global counts, tagged with
|
|
94422
|
-
* the device that happened to close the window — the counters are the
|
|
94423
|
-
* gate's, not the camera's, and the tag is there because every line about a
|
|
94424
|
-
* device carries one.
|
|
94425
|
-
*
|
|
94426
|
-
* `materialised` is the number of 4096-px native full-frame fetches this
|
|
94427
|
-
* path actually spent; `byGap` + `byMargin` + `byWorse` is what it did not.
|
|
94428
|
-
*/
|
|
94429
|
-
logGroupShotWindow(deviceId) {
|
|
94430
|
-
const now = Date.now();
|
|
94431
|
-
if (now - this.lastGroupShotMetricAt < GROUP_SHOT_METRIC_INTERVAL_MS) return;
|
|
94432
|
-
this.lastGroupShotMetricAt = now;
|
|
94433
|
-
const counts = this.groupShots.drainCounts();
|
|
94434
|
-
if (counts.offered === 0) return;
|
|
94435
|
-
this.ctx.logger.info("group shot window", {
|
|
94436
|
-
tags: { deviceId },
|
|
94437
|
-
meta: {
|
|
94438
|
-
offered: counts.offered,
|
|
94439
|
-
materialised: counts.materialised,
|
|
94440
|
-
suppressedByGap: counts.byGap,
|
|
94441
|
-
suppressedByMargin: counts.byMargin,
|
|
94442
|
-
suppressedByWorse: counts.byWorse,
|
|
94443
|
-
openSummaries: this.groupShots.size
|
|
94444
|
-
}
|
|
94445
|
-
});
|
|
94446
|
-
}
|
|
94447
|
-
/**
|
|
94448
92233
|
* Order the parcels a best-frame decision wants (D422). Skipped entirely at
|
|
94449
92234
|
* a node the registry already called offline (D58) — logged on the
|
|
94450
92235
|
* transition by `noteCapturesGated`, counted in the key-frame window.
|
|
@@ -94481,6 +92266,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
94481
92266
|
timestamp: parcel.timestamp
|
|
94482
92267
|
};
|
|
94483
92268
|
}
|
|
92269
|
+
/**
|
|
92270
|
+
* Store each given track's clean native `keyFrame` + `keyFrameSmall` from its
|
|
92271
|
+
* PARKED parcel (D422) — never a by-handle fetch. The body is
|
|
92272
|
+
* `pipeline/key-frame-persist.ts`; what stays here is the scheduling
|
|
92273
|
+
* (coalescing, priority, budget) and the bookkeeping the retry gate reads.
|
|
92274
|
+
* Best-effort (D8): a per-track failure is logged and never thrown.
|
|
92275
|
+
*/
|
|
94484
92276
|
async persistKeyFrames(deviceId, timestamp, captures) {
|
|
94485
92277
|
const mediaStore = this.mediaStore;
|
|
94486
92278
|
if (this.parkedFrames === null || !mediaStore) return;
|
|
@@ -94581,7 +92373,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
94581
92373
|
meta: {
|
|
94582
92374
|
trackId,
|
|
94583
92375
|
keyFrameMediaId,
|
|
94584
|
-
error: errMsg
|
|
92376
|
+
error: errMsg(err)
|
|
94585
92377
|
}
|
|
94586
92378
|
});
|
|
94587
92379
|
}
|
|
@@ -94648,8 +92440,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
94648
92440
|
* when it wins and the frame's pixels are still reachable. Callers: the
|
|
94649
92441
|
* FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
|
|
94650
92442
|
* fine-classification attribution. */
|
|
94651
|
-
promoteSemanticRecognition(input
|
|
94652
|
-
if (origin === "identified") this.recognizedTrackIds.add(input.trackId);
|
|
92443
|
+
promoteSemanticRecognition(input) {
|
|
94653
92444
|
const ctx = this.ctxIfReady;
|
|
94654
92445
|
if (ctx === null) return;
|
|
94655
92446
|
promoteSemanticBestFrame({
|
|
@@ -95190,7 +92981,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
95190
92981
|
tags: { deviceId: input.deviceId },
|
|
95191
92982
|
meta: {
|
|
95192
92983
|
className: ref.className,
|
|
95193
|
-
error: errMsg
|
|
92984
|
+
error: errMsg(err)
|
|
95194
92985
|
}
|
|
95195
92986
|
});
|
|
95196
92987
|
}
|
|
@@ -95222,7 +93013,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
95222
93013
|
tags: { deviceId: track.deviceId },
|
|
95223
93014
|
meta: {
|
|
95224
93015
|
episodeId: ref.episodeId,
|
|
95225
|
-
error: errMsg
|
|
93016
|
+
error: errMsg(err)
|
|
95226
93017
|
}
|
|
95227
93018
|
});
|
|
95228
93019
|
}
|
|
@@ -95479,7 +93270,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
95479
93270
|
tags: { deviceId: input.deviceId },
|
|
95480
93271
|
meta: {
|
|
95481
93272
|
eventId: input.eventId,
|
|
95482
|
-
error: errMsg
|
|
93273
|
+
error: errMsg(err)
|
|
95483
93274
|
}
|
|
95484
93275
|
});
|
|
95485
93276
|
});
|
|
@@ -95546,7 +93337,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
95546
93337
|
async sweepExpiredTracks() {
|
|
95547
93338
|
if (this.shuttingDown || !this.trackStore) return;
|
|
95548
93339
|
await this.trackCloser.sweep();
|
|
95549
|
-
await this.summarySealer?.sweep();
|
|
95550
93340
|
}
|
|
95551
93341
|
/**
|
|
95552
93342
|
* The follow-recordings floor per camera, read from the ARCHIVE's hour ledger
|
|
@@ -95586,7 +93376,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
95586
93376
|
listTrackedDeviceIds: async () => await this.trackStore?.listDeviceIds() ?? []
|
|
95587
93377
|
}, (half, err) => this.ctx.logger.warn("retention scope half unavailable — sweeping on the other", { meta: {
|
|
95588
93378
|
half,
|
|
95589
|
-
error: errMsg
|
|
93379
|
+
error: errMsg(err)
|
|
95590
93380
|
} }));
|
|
95591
93381
|
}
|
|
95592
93382
|
/**
|
|
@@ -95633,7 +93423,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
95633
93423
|
await this.runTrackRetentionSweep(deviceIds, now);
|
|
95634
93424
|
const eventStore = this.eventStore;
|
|
95635
93425
|
const sensorEventStore = this.sensorEventStore;
|
|
95636
|
-
const summaryStore = this.summaryStore;
|
|
95637
93426
|
await sweepTracklessRoots({
|
|
95638
93427
|
deviceIds,
|
|
95639
93428
|
nowMs: now,
|
|
@@ -95643,16 +93432,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
95643
93432
|
cutoffMs
|
|
95644
93433
|
}),
|
|
95645
93434
|
evictSensorEvents: sensorEventStore ? (deviceId, cutoffMs) => sensorEventStore.evictBefore(deviceId, cutoffMs) : null,
|
|
95646
|
-
evictSummaries: summaryStore ? (deviceId, cutoffMs) => summaryStore.evictBefore(deviceId, cutoffMs) : null,
|
|
95647
93435
|
onEvicted: async (deviceId, evicted, cutoffs) => {
|
|
95648
|
-
const total = evicted.motion.length + evicted.audio.length + evicted.sensor
|
|
93436
|
+
const total = evicted.motion.length + evicted.audio.length + evicted.sensor;
|
|
95649
93437
|
await this.eventsOpsLog?.append({
|
|
95650
93438
|
op: "prune",
|
|
95651
93439
|
reason: "retention",
|
|
95652
93440
|
deviceId,
|
|
95653
93441
|
itemsAffected: total,
|
|
95654
93442
|
bytesReclaimed: 0,
|
|
95655
|
-
detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor
|
|
93443
|
+
detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor`
|
|
95656
93444
|
});
|
|
95657
93445
|
this.ctx.logger.info("analytics trackless event eviction (age sweep)", {
|
|
95658
93446
|
tags: { deviceId },
|
|
@@ -95660,7 +93448,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
95660
93448
|
motion: evicted.motion.length,
|
|
95661
93449
|
audio: evicted.audio.length,
|
|
95662
93450
|
sensor: evicted.sensor,
|
|
95663
|
-
summaries: evicted.summaries,
|
|
95664
93451
|
mode: cutoffs.effectiveMode,
|
|
95665
93452
|
cutoffMs: cutoffs.cutoffMs
|
|
95666
93453
|
}
|
|
@@ -96614,118 +94401,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96614
94401
|
async listRecentTracks(input) {
|
|
96615
94402
|
return this.queryFacade.listRecentTracks(input);
|
|
96616
94403
|
}
|
|
96617
|
-
/** Newest first for a camera set; each row carries its group-shot media key. */
|
|
96618
|
-
async listSummaries(input) {
|
|
96619
|
-
const store = this.summaryStore;
|
|
96620
|
-
if (!store) return { summaries: [] };
|
|
96621
|
-
const rows = await store.listRecent({
|
|
96622
|
-
deviceIds: input.deviceIds,
|
|
96623
|
-
...input.since !== void 0 ? { since: input.since } : {},
|
|
96624
|
-
...input.until !== void 0 ? { until: input.until } : {},
|
|
96625
|
-
limit: input.limit ?? 50,
|
|
96626
|
-
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
96627
|
-
});
|
|
96628
|
-
return { summaries: await this.listItemsFor(rows) };
|
|
96629
|
-
}
|
|
96630
|
-
/**
|
|
96631
|
-
* Rows → what a LIST draws: the envelope plus the composition.
|
|
96632
|
-
*
|
|
96633
|
-
* Two bulk reads for the whole page, never per row: the members in one, their
|
|
96634
|
-
* tracks (for the names) in one. The MEMBERS themselves stay off the list —
|
|
96635
|
-
* a page of two hundred groups would otherwise carry a thousand records so
|
|
96636
|
-
* the client could count them. `getSummary` is where the links live.
|
|
96637
|
-
*/
|
|
96638
|
-
async listItemsFor(rows) {
|
|
96639
|
-
const store = this.summaryStore;
|
|
96640
|
-
if (!store || rows.length === 0) return [];
|
|
96641
|
-
const members = await store.membersOfMany(rows.map((r) => r.id));
|
|
96642
|
-
return await Promise.all(rows.map(async (row) => ({
|
|
96643
|
-
summary: await this.toSummary(row),
|
|
96644
|
-
...summaryComposition(members.get(row.id) ?? [])
|
|
96645
|
-
})));
|
|
96646
|
-
}
|
|
96647
|
-
/** The members of a page of summaries, with the labels a card can show —
|
|
96648
|
-
* in ONE read of the members and ONE of their tracks. */
|
|
96649
|
-
async labelledMembersFor(rows) {
|
|
96650
|
-
const out = /* @__PURE__ */ new Map();
|
|
96651
|
-
const store = this.summaryStore;
|
|
96652
|
-
if (!store || rows.length === 0) return out;
|
|
96653
|
-
const membersBySummary = await store.membersOfMany(rows.map((r) => r.id));
|
|
96654
|
-
const trackIds = [...new Set([...membersBySummary.values()].flatMap((ms) => ms.map((m) => m.trackId)))];
|
|
96655
|
-
let tracks = [];
|
|
96656
|
-
try {
|
|
96657
|
-
tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
|
|
96658
|
-
} catch (err) {
|
|
96659
|
-
this.ctx.logger.warn("summary members: labels unavailable for this page", { meta: {
|
|
96660
|
-
summaries: rows.length,
|
|
96661
|
-
members: trackIds.length,
|
|
96662
|
-
error: errMsg$1(err)
|
|
96663
|
-
} });
|
|
96664
|
-
}
|
|
96665
|
-
for (const row of rows) out.set(row.id, withMemberLabels(membersBySummary.get(row.id) ?? [], tracks));
|
|
96666
|
-
return out;
|
|
96667
|
-
}
|
|
96668
|
-
/**
|
|
96669
|
-
* Rows → the shape every summary surface answers with: the envelope, its
|
|
96670
|
-
* members, and what a thumbnail can render for each of them.
|
|
96671
|
-
*
|
|
96672
|
-
* THREE reads for the whole page, never per row: the members in one, their
|
|
96673
|
-
* tracks in one, and the group shots through `toSummary`. A read per member
|
|
96674
|
-
* is what made a sixty-group page take twenty seconds on the events feed.
|
|
96675
|
-
*/
|
|
96676
|
-
/** ONE summary, with every member and the link each of them carries. */
|
|
96677
|
-
async detailFor(row) {
|
|
96678
|
-
const labelled = (await this.labelledMembersFor([row])).get(row.id) ?? [];
|
|
96679
|
-
return {
|
|
96680
|
-
summary: await this.toSummary(row),
|
|
96681
|
-
members: labelled.map((m) => ({
|
|
96682
|
-
trackId: m.trackId,
|
|
96683
|
-
className: m.className,
|
|
96684
|
-
role: m.role,
|
|
96685
|
-
joinedAt: m.joinedAt,
|
|
96686
|
-
...m.label !== void 0 ? { label: m.label } : {},
|
|
96687
|
-
...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
|
|
96688
|
-
}))
|
|
96689
|
-
};
|
|
96690
|
-
}
|
|
96691
|
-
async listRecentSummaries(input) {
|
|
96692
|
-
const store = this.summaryStore;
|
|
96693
|
-
if (!store) return {
|
|
96694
|
-
summaries: [],
|
|
96695
|
-
nextCursor: null
|
|
96696
|
-
};
|
|
96697
|
-
const page = await store.queryRecent({
|
|
96698
|
-
deviceIds: input.deviceIds,
|
|
96699
|
-
...input.since !== void 0 ? { since: input.since } : {},
|
|
96700
|
-
...input.until !== void 0 ? { until: input.until } : {},
|
|
96701
|
-
limit: input.limit,
|
|
96702
|
-
...input.cursor !== void 0 ? { cursor: input.cursor } : {},
|
|
96703
|
-
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
96704
|
-
});
|
|
96705
|
-
return {
|
|
96706
|
-
summaries: await this.listItemsFor(page.summaries.map((d) => d.summary)),
|
|
96707
|
-
nextCursor: page.nextCursor
|
|
96708
|
-
};
|
|
96709
|
-
}
|
|
96710
|
-
async getSummary(input) {
|
|
96711
|
-
const detail = await this.summaryStore?.get(input.id);
|
|
96712
|
-
if (!detail) return null;
|
|
96713
|
-
return await this.detailFor(detail.summary);
|
|
96714
|
-
}
|
|
96715
|
-
/** The row plus its group shot: the web companion when it landed, else the native key frame. */
|
|
96716
|
-
async toSummary(row) {
|
|
96717
|
-
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|
|
96718
|
-
const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
|
|
96719
|
-
return {
|
|
96720
|
-
id: row.id,
|
|
96721
|
-
deviceId: row.deviceId,
|
|
96722
|
-
openedAt: row.openedAt,
|
|
96723
|
-
lastActiveAt: row.lastActiveAt,
|
|
96724
|
-
sealedAt: row.sealedAt,
|
|
96725
|
-
memberCount: row.memberCount,
|
|
96726
|
-
groupShotMediaId: shot?.mediaId ?? null
|
|
96727
|
-
};
|
|
96728
|
-
}
|
|
96729
94404
|
/**
|
|
96730
94405
|
* Every event kind the device can produce: built-ins (motion + audio), the
|
|
96731
94406
|
* detection classes its CONFIGURED pipeline can emit, and sensor kinds from
|
|
@@ -96754,7 +94429,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96754
94429
|
tags: { deviceId: input.deviceId },
|
|
96755
94430
|
meta: {
|
|
96756
94431
|
scope,
|
|
96757
|
-
error: errMsg
|
|
94432
|
+
error: errMsg(err)
|
|
96758
94433
|
}
|
|
96759
94434
|
})
|
|
96760
94435
|
}, input.deviceId);
|
|
@@ -96772,7 +94447,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96772
94447
|
} catch (err) {
|
|
96773
94448
|
this.ctx.logger.warn(`${scope}: resolvePipeline failed — no ML kinds advertised`, {
|
|
96774
94449
|
tags: { deviceId },
|
|
96775
|
-
meta: { error: errMsg
|
|
94450
|
+
meta: { error: errMsg(err) }
|
|
96776
94451
|
});
|
|
96777
94452
|
return NO_EMITTABLE;
|
|
96778
94453
|
}
|
|
@@ -96809,7 +94484,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96809
94484
|
}, deviceIds).catch((err) => {
|
|
96810
94485
|
this.ctx.logger.warn("listEventKindsBatch: batched device topology unavailable — per-device fallback", { meta: {
|
|
96811
94486
|
deviceCount: deviceIds.length,
|
|
96812
|
-
error: errMsg
|
|
94487
|
+
error: errMsg(err)
|
|
96813
94488
|
} });
|
|
96814
94489
|
return null;
|
|
96815
94490
|
});
|
|
@@ -96827,13 +94502,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96827
94502
|
tags: { deviceId },
|
|
96828
94503
|
meta: {
|
|
96829
94504
|
scope,
|
|
96830
|
-
error: errMsg
|
|
94505
|
+
error: errMsg(err)
|
|
96831
94506
|
}
|
|
96832
94507
|
})
|
|
96833
94508
|
}, deviceId);
|
|
96834
94509
|
}, (deviceId, err) => this.ctx.logger.warn("listEventKindsBatch: compose threw — empty vocabulary", {
|
|
96835
94510
|
tags: { deviceId },
|
|
96836
|
-
meta: { error: errMsg
|
|
94511
|
+
meta: { error: errMsg(err) }
|
|
96837
94512
|
}));
|
|
96838
94513
|
}
|
|
96839
94514
|
/** Per-camera sensor-event history (state changes of linked devices). */
|
|
@@ -96863,7 +94538,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96863
94538
|
meta: {
|
|
96864
94539
|
capName: data.capName,
|
|
96865
94540
|
cameraIds: [...cameras.mediaCameraIds],
|
|
96866
|
-
error: errMsg
|
|
94541
|
+
error: errMsg(err)
|
|
96867
94542
|
}
|
|
96868
94543
|
});
|
|
96869
94544
|
return;
|
|
@@ -96907,7 +94582,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96907
94582
|
meta: {
|
|
96908
94583
|
kind: event.kind,
|
|
96909
94584
|
linkedCameraIds: [...input.mediaCameraIds],
|
|
96910
|
-
error: errMsg
|
|
94585
|
+
error: errMsg(err)
|
|
96911
94586
|
}
|
|
96912
94587
|
});
|
|
96913
94588
|
}
|
|
@@ -96924,7 +94599,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96924
94599
|
tags: { deviceId: data.deviceId },
|
|
96925
94600
|
meta: {
|
|
96926
94601
|
capName: data.capName,
|
|
96927
|
-
error: errMsg
|
|
94602
|
+
error: errMsg(err)
|
|
96928
94603
|
}
|
|
96929
94604
|
});
|
|
96930
94605
|
});
|
|
@@ -96980,7 +94655,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
96980
94655
|
tags: { deviceId: data.deviceId },
|
|
96981
94656
|
meta: {
|
|
96982
94657
|
capName: data.capName,
|
|
96983
|
-
error: errMsg
|
|
94658
|
+
error: errMsg(err)
|
|
96984
94659
|
}
|
|
96985
94660
|
});
|
|
96986
94661
|
return NO_SENSOR_NOTIFICATION_CAMERAS;
|
|
@@ -97002,7 +94677,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
97002
94677
|
tags: { deviceId: data.deviceId },
|
|
97003
94678
|
meta: {
|
|
97004
94679
|
capName: data.capName,
|
|
97005
|
-
error: errMsg
|
|
94680
|
+
error: errMsg(err)
|
|
97006
94681
|
}
|
|
97007
94682
|
});
|
|
97008
94683
|
}
|
|
@@ -97074,7 +94749,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
97074
94749
|
tags: { deviceId: data.deviceId },
|
|
97075
94750
|
meta: {
|
|
97076
94751
|
capName: data.capName,
|
|
97077
|
-
error: errMsg
|
|
94752
|
+
error: errMsg(err)
|
|
97078
94753
|
}
|
|
97079
94754
|
});
|
|
97080
94755
|
}
|
|
@@ -97097,7 +94772,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
97097
94772
|
meta: {
|
|
97098
94773
|
kind: ev.kind,
|
|
97099
94774
|
sourceDeviceId: ev.sourceDeviceId,
|
|
97100
|
-
error: errMsg
|
|
94775
|
+
error: errMsg(err)
|
|
97101
94776
|
}
|
|
97102
94777
|
});
|
|
97103
94778
|
}
|
|
@@ -97117,7 +94792,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
97117
94792
|
meta: {
|
|
97118
94793
|
kind: ev.kind,
|
|
97119
94794
|
linkedCameraIds: [...linkedCameraIds],
|
|
97120
|
-
error: errMsg
|
|
94795
|
+
error: errMsg(err)
|
|
97121
94796
|
}
|
|
97122
94797
|
});
|
|
97123
94798
|
}
|
|
@@ -97247,7 +94922,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
97247
94922
|
} catch (err) {
|
|
97248
94923
|
this.ctx?.logger?.debug("resolveProducingDeviceName failed", {
|
|
97249
94924
|
tags: { deviceId },
|
|
97250
|
-
meta: { error: errMsg
|
|
94925
|
+
meta: { error: errMsg(err) }
|
|
97251
94926
|
});
|
|
97252
94927
|
return;
|
|
97253
94928
|
}
|
|
@@ -97509,11 +95184,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
97509
95184
|
},
|
|
97510
95185
|
liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
|
|
97511
95186
|
liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
|
|
97512
|
-
liveSummaryIds: (ids) => {
|
|
97513
|
-
const summaries = this.summaryStore;
|
|
97514
|
-
if (!summaries) throw new Error("summary store not built — probe unavailable");
|
|
97515
|
-
return summaries.existingIds(ids);
|
|
97516
|
-
},
|
|
97517
95187
|
keptMediaKeys: async (keys) => this.keptStationaryMediaKeys(keys),
|
|
97518
95188
|
reclaim: buildOrphanReclaimer({
|
|
97519
95189
|
deleteFace: async (faceId) => {
|
|
@@ -98120,7 +95790,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
98120
95790
|
nodeId
|
|
98121
95791
|
};
|
|
98122
95792
|
} catch (err) {
|
|
98123
|
-
const reason = errMsg
|
|
95793
|
+
const reason = errMsg(err);
|
|
98124
95794
|
failures.push(`${nodeId}: ${reason}`);
|
|
98125
95795
|
this.ctx.logger.warn("photo enrollment: a node could not analyse the photograph", { meta: {
|
|
98126
95796
|
nodeId,
|
|
@@ -98846,7 +96516,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
98846
96516
|
this.unreadableClipIndexes.add(space.index);
|
|
98847
96517
|
this.ctx.logger.warn("embedding.rebuildStatus: index size unreadable — reported as unknown", { meta: {
|
|
98848
96518
|
index: space.index,
|
|
98849
|
-
error: errMsg
|
|
96519
|
+
error: errMsg(err)
|
|
98850
96520
|
} });
|
|
98851
96521
|
}
|
|
98852
96522
|
indexedByIndex[space.index] = null;
|
|
@@ -99202,8 +96872,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
99202
96872
|
mediaStore,
|
|
99203
96873
|
faceStore: this.faceStore,
|
|
99204
96874
|
plateStore: this.plateStore,
|
|
99205
|
-
objectEmbeddingStore: this.objectEmbeddingStore
|
|
99206
|
-
summaryStore: this.summaryStore
|
|
96875
|
+
objectEmbeddingStore: this.objectEmbeddingStore
|
|
99207
96876
|
};
|
|
99208
96877
|
const absent = absentCascadeLegs(stores);
|
|
99209
96878
|
if (absent.length > 0) this.ctx.logger.warn("track cascade is missing a leg — its rows will be left behind", { meta: {
|
|
@@ -99781,7 +97450,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
99781
97450
|
this.ctx.logger.debug("event media: thumb variant failed — serving full", { meta: {
|
|
99782
97451
|
mediaId: media.mediaId,
|
|
99783
97452
|
size: variant.size,
|
|
99784
|
-
error: errMsg
|
|
97453
|
+
error: errMsg(err)
|
|
99785
97454
|
} });
|
|
99786
97455
|
return media;
|
|
99787
97456
|
}
|