@camstack/addon-post-analysis 1.2.159 → 1.2.160
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-D_O7oxyS.mjs → dist-BnxvdGGq.mjs} +14 -76
- package/dist/{dist-CLxPUxVN.js → dist-WrUmjaGQ.js} +13 -87
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B8GyXydu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BUiTkbfu.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Be4GMf6Z.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-CtAUDi3k.mjs → hostInit-ByjIzasm.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +63 -1633
- package/dist/pipeline-analytics/index.mjs +55 -1624
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +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-B0wvjNmC.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as buildEventKindDescriptor, A as NcSnoozeSuppressedSchema, At as DeviceType, B as SCENE_DIVERGED, Bt as literal, C as NcRuleInputSchema, Ct as subKindsOf, D as NcScheduleSchema, Dt as zoneAnalyticsCapability, E as NcRuleTargetSchema, Et as videoclipsCapability, F as RECORDING_EXPORT_MAX_READ_BYTES, Ft as sleep$1, G as TimelapseRuleSchema, Gt as string, H as TIMELAPSE_DENSE_FLOOR_SEC, Ht as object, I as RetrainStatusSchema, It as _enum, J as addonWidgetsSourceCapability, K as TrackSourceSchema, Kt as unknown, Lt as array, M as NcTaxonomySchema, Mt as hydrateSchema, N as OpsLogEntrySchema, Nt as isDeviceScopedCap, O as NcSnoozeInputSchema, Ot as errMsg, Pt as nodePin, Q as audioModeOf, R as SCENE_DEFAULT_ANCHOR_THRESHOLD, Rt as boolean, S as NcConditionDescriptorSchema, St as sceneMonitorCapability, T as NcRuleSchema, Tt as vectorDimFromBase64, U as TimelapseRuleInputSchema, Ut as partialRecord, V as SceneMonitorSchema, Vt as number, W as TimelapseRulePatchSchema, Wt as record, X as assertTimelapseCadences, Y as alarmPanelCapability, Z as audioMetricsCapability, _ as NC_ALARM_SYSTEM_EVENT_KINDS, _t as pipelineAnalyticsCapability, at as encodeVectorBase64, bt as readTimelapseGeneratedAt, ct as failureContributionCapability, d as EVENT_PAD_MS, dt as isScheduleActive, et as cosineSimilarity$1, f as FULL_IMAGE_BBOX, ft as isSourceCap, g as MediaFileKindEnum, gt as pickClusterStepModels, h as MACRO_LABELS, i as DEFAULT_EVENT_COLOR, j as NcSystemEventKindSchema, jt as createEvent, k as NcSnoozeSchema, kt as BaseAddon, l as DeclaredDevices, m as LabelAttributionSchema, mt as notificationRulesCapability, n as CLUSTER_MODEL_SCOPED_STEPS, nt as defineCustomActions, ot as evaluateSensorEdge, p as FailureCounters, pt as kebabToCamel, q as VISIT_MERGE_GAP_MS, qt as EventCategory, r as COCO_TO_MACRO, rt as deriveRecordingMode, s as DETECTION_MACRO_CLASSES, st as faceGalleryCapability, t as BaseDevice, tt as customAction, u as EVENT_KIND_BY_CAP, ut as isDetectionMacroClass, v as NC_CONDITION_CATALOG, vt as plateGalleryCapability, w as NcRulePatchSchema, wt as systemEventFilterApplies, x as NC_TAXONOMY, y as NC_DEFAULT_SNOOZE_MINUTES, yt as readDeviceStateFrom, z as SCENE_DEFAULT_UNCOVERED_POLICY, zt as discriminatedUnion } from "../dist-BnxvdGGq.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";
|
|
@@ -15690,10 +15690,9 @@ function remainingSummaryCandidates(kept, taken, rank) {
|
|
|
15690
15690
|
/**
|
|
15691
15691
|
* The media a tile is drawn from, best first.
|
|
15692
15692
|
*
|
|
15693
|
-
* A digest tile is a SUBJECT, so the crops come first: the
|
|
15694
|
-
* crop (`
|
|
15695
|
-
*
|
|
15696
|
-
* then the clean whole scene (`keyFrame` → `keyFrameSmall` → `fullFrame`).
|
|
15693
|
+
* A digest tile is a SUBJECT, so the crops come first: the TRACK's own subject
|
|
15694
|
+
* crop (`thumbnail`, then its `thumbnailSmall` downscale), and only then the
|
|
15695
|
+
* clean whole scene (`keyFrame` → `keyFrameSmall` → `fullFrame`).
|
|
15697
15696
|
*
|
|
15698
15697
|
* ⚠ The whole-scene rungs are a FALLBACK and stay on the ladder. Measured on
|
|
15699
15698
|
* 60 real tracks (615/617, 24 h): 52 carry a crop, 43 carry a scene rung, and
|
|
@@ -15701,19 +15700,19 @@ function remainingSummaryCandidates(kept, taken, rank) {
|
|
|
15701
15700
|
* two tiles for nothing. A track with neither is dropped, never drawn as an
|
|
15702
15701
|
* empty cell (see `NcSummaryProducer.loadTiles`).
|
|
15703
15702
|
*
|
|
15704
|
-
* ## Why `thumbnail` is
|
|
15703
|
+
* ## Why `thumbnail` is on the ladder (2026-08-27)
|
|
15705
15704
|
*
|
|
15706
15705
|
* It was removed on 2026-08-24 (160200681) because a successful plate read
|
|
15707
15706
|
* overwrites the vehicle first-level crop under the same `thumbnail` kind.
|
|
15708
|
-
* **That collision
|
|
15709
|
-
* `ownerKind: 'group', kind: 'thumbnail'
|
|
15710
|
-
*
|
|
15711
|
-
*
|
|
15712
|
-
*
|
|
15713
|
-
*
|
|
15714
|
-
*
|
|
15715
|
-
* `scripts/check-summary-ladder-reachable.ts` is the guard that
|
|
15716
|
-
*
|
|
15707
|
+
* **That collision belonged to the GROUP entity** — its sealer was what wrote
|
|
15708
|
+
* `ownerKind: 'group', kind: 'thumbnail'`. On a TRACK, plates live under
|
|
15709
|
+
* `ownerKind: 'plate'` / `kind: 'plateCrop'` and never touch `thumbnail`. The
|
|
15710
|
+
* same commit turned the group process off, so the two group-only head rungs
|
|
15711
|
+
* became unreachable and every digest tile fell through to `keyFrame` — a
|
|
15712
|
+
* whole frame — for three days. The group entity was deleted outright on
|
|
15713
|
+
* 2026-09-01 and its rungs went with it;
|
|
15714
|
+
* `scripts/check-summary-ladder-reachable.ts` is the guard that keeps the
|
|
15715
|
+
* best reachable rung a subject crop.
|
|
15717
15716
|
*
|
|
15718
15717
|
* Boxed frames stay absent: a drawn rectangle is a permanent lie in a digest
|
|
15719
15718
|
* tile. Plate and face crops stay absent: a digest tile of a targa is the
|
|
@@ -15721,8 +15720,6 @@ function remainingSummaryCandidates(kept, taken, rank) {
|
|
|
15721
15720
|
* `frameDedupeKey`, not by hiding the whole-scene fallback.
|
|
15722
15721
|
*/
|
|
15723
15722
|
var SUMMARY_MEDIA_LADDER = [
|
|
15724
|
-
"groupCrop",
|
|
15725
|
-
"nativeFrame",
|
|
15726
15723
|
"thumbnail",
|
|
15727
15724
|
"thumbnailSmall",
|
|
15728
15725
|
"keyFrame",
|
|
@@ -15757,7 +15754,8 @@ var SUMMARY_SIZE_GATED_RUNGS = new Set(["thumbnail"]);
|
|
|
15757
15754
|
* Is this row a picture, or a template wearing a picture's kind?
|
|
15758
15755
|
*
|
|
15759
15756
|
* Only a size the index REPORTED can demote a row. A `null` size is unknown,
|
|
15760
|
-
* and demoting on unknown would silently delete every
|
|
15757
|
+
* and demoting on unknown would silently delete every row an index could not
|
|
15758
|
+
* measure.
|
|
15761
15759
|
*/
|
|
15762
15760
|
function rowClearsSizeFloor(row, rung) {
|
|
15763
15761
|
if (!SUMMARY_SIZE_GATED_RUNGS.has(rung)) return true;
|
|
@@ -25936,10 +25934,29 @@ var OWNER_KINDS = [
|
|
|
25936
25934
|
"identity",
|
|
25937
25935
|
"plate",
|
|
25938
25936
|
"vehicle",
|
|
25939
|
-
"scene"
|
|
25940
|
-
"group"
|
|
25937
|
+
"scene"
|
|
25941
25938
|
];
|
|
25942
25939
|
/**
|
|
25940
|
+
* Owner kinds that NOTHING may write any more, but whose rows can still be on
|
|
25941
|
+
* disk from before the owning code was deleted.
|
|
25942
|
+
*
|
|
25943
|
+
* `'group'` is the co-moving group entity, removed on 2026-09-01 together with
|
|
25944
|
+
* `pipeline-analytics:track-groups` / `:track-group-members`
|
|
25945
|
+
* ([D183](../../../../../docs/decisions/adr-0183-code-that-is-deleted-takes-its-tables-with-it.md)).
|
|
25946
|
+
* The tables are dropped at boot from `RETIRED_COLLECTIONS`, so a group-owned
|
|
25947
|
+
* media row can never be claimed again: `resolveMediaOwner` maps it to
|
|
25948
|
+
* `{ kind: 'retired-owner' }` and the orphan audit reclaims it under the same
|
|
25949
|
+
* grace window and ceilings as any other orphan.
|
|
25950
|
+
*
|
|
25951
|
+
* Deliberately NOT a member of {@link OWNER_KINDS}: the type is what stops a
|
|
25952
|
+
* new writer, and this list is only what the READ side must still recognise.
|
|
25953
|
+
*/
|
|
25954
|
+
var RETIRED_OWNER_KINDS = ["group"];
|
|
25955
|
+
/** Is this an owner kind whose owning code is gone? See {@link RETIRED_OWNER_KINDS}. */
|
|
25956
|
+
function isRetiredMediaOwnerKind(value) {
|
|
25957
|
+
return RETIRED_OWNER_KINDS.includes(value);
|
|
25958
|
+
}
|
|
25959
|
+
/**
|
|
25943
25960
|
* Narrow an arbitrary string to an `OwnerKind`.
|
|
25944
25961
|
*
|
|
25945
25962
|
* Exists so a caller that builds owner scopes from data (the training-export
|
|
@@ -27069,10 +27086,6 @@ var MediaStore = class {
|
|
|
27069
27086
|
async deleteForTracks(trackIds) {
|
|
27070
27087
|
return this.deleteForOwner("track", trackIds);
|
|
27071
27088
|
}
|
|
27072
|
-
/** Delete the group's OWN crop. The native frame is a pointer and is not here. */
|
|
27073
|
-
async deleteByGroups(groupIds) {
|
|
27074
|
-
return this.deleteForOwnerCounted("group", groupIds);
|
|
27075
|
-
}
|
|
27076
27089
|
/**
|
|
27077
27090
|
* `TrackScopedStore` contract for the retention cascade: delete all media
|
|
27078
27091
|
* owned by the given tracks — the tracks' OWN media (`ownerKind:'track'`) plus
|
|
@@ -33938,7 +33951,6 @@ var EMPTY_CASCADE_TALLY = {
|
|
|
33938
33951
|
faces: 0,
|
|
33939
33952
|
plates: 0,
|
|
33940
33953
|
embeddings: 0,
|
|
33941
|
-
groups: 0,
|
|
33942
33954
|
bytesReclaimed: 0
|
|
33943
33955
|
};
|
|
33944
33956
|
/** Fold a leaf store's receipts into a running tally. */
|
|
@@ -33982,13 +33994,6 @@ function addLeafCounts(tally, counts) {
|
|
|
33982
33994
|
bytesReclaimed
|
|
33983
33995
|
};
|
|
33984
33996
|
break;
|
|
33985
|
-
case "groups":
|
|
33986
|
-
next = {
|
|
33987
|
-
...next,
|
|
33988
|
-
groups: next.groups + c.rows,
|
|
33989
|
-
bytesReclaimed
|
|
33990
|
-
};
|
|
33991
|
-
break;
|
|
33992
33997
|
}
|
|
33993
33998
|
}
|
|
33994
33999
|
return next;
|
|
@@ -34002,7 +34007,6 @@ function mergeCascadeTallies(a, b) {
|
|
|
34002
34007
|
faces: a.faces + b.faces,
|
|
34003
34008
|
plates: a.plates + b.plates,
|
|
34004
34009
|
embeddings: a.embeddings + b.embeddings,
|
|
34005
|
-
groups: a.groups + b.groups,
|
|
34006
34010
|
bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
|
|
34007
34011
|
};
|
|
34008
34012
|
}
|
|
@@ -34065,7 +34069,6 @@ function absentCascadeLegs(stores) {
|
|
|
34065
34069
|
if (!stores.faceStore) absent.push("faces");
|
|
34066
34070
|
if (!stores.plateStore) absent.push("plates");
|
|
34067
34071
|
if (!stores.objectEmbeddingStore) absent.push("embeddings");
|
|
34068
|
-
if (!stores.groupStore) absent.push("groups");
|
|
34069
34072
|
return absent;
|
|
34070
34073
|
}
|
|
34071
34074
|
function buildAnalyticsCascadeRegistry(stores) {
|
|
@@ -34073,7 +34076,6 @@ function buildAnalyticsCascadeRegistry(stores) {
|
|
|
34073
34076
|
if (stores.faceStore) registry.push(stores.faceStore);
|
|
34074
34077
|
if (stores.plateStore) registry.push(stores.plateStore);
|
|
34075
34078
|
if (stores.objectEmbeddingStore) registry.push(stores.objectEmbeddingStore);
|
|
34076
|
-
if (stores.groupStore) registry.push(stores.groupStore);
|
|
34077
34079
|
return registry;
|
|
34078
34080
|
}
|
|
34079
34081
|
/**
|
|
@@ -47769,1419 +47771,6 @@ function shouldAutoReembed(samples, targetModelId, knownPixelless, passRunning)
|
|
|
47769
47771
|
if (passRunning || targetModelId === null || targetModelId === "") return false;
|
|
47770
47772
|
return samples.some((s) => s.modelId !== targetModelId && s.mediaKey !== void 0 && !knownPixelless.has(s.sampleId));
|
|
47771
47773
|
}
|
|
47772
|
-
var groupActions = defineCustomActions({
|
|
47773
|
-
"analytics.listGroups": customAction(object({
|
|
47774
|
-
deviceId: number().int().optional(),
|
|
47775
|
-
sinceMs: number().int().optional(),
|
|
47776
|
-
untilMs: number().int().optional(),
|
|
47777
|
-
limit: number().int().min(1).max(100).optional(),
|
|
47778
|
-
offset: number().int().min(0).optional()
|
|
47779
|
-
}), object({
|
|
47780
|
-
groups: array(AnalyticsGroupRecordSchema).readonly(),
|
|
47781
|
-
hasMore: boolean()
|
|
47782
|
-
})),
|
|
47783
|
-
"analytics.getGroup": customAction(object({ groupId: string().min(1) }), object({ group: AnalyticsGroupDetailSchema.nullable() }))
|
|
47784
|
-
});
|
|
47785
|
-
var MATCH_WINDOW_MS = 1e3;
|
|
47786
|
-
var MOVE_EPS = .35;
|
|
47787
|
-
/** Live-assigner window: same predicate, recent positions only. */
|
|
47788
|
-
var LIVE_WINDOW_MS = 3e3;
|
|
47789
|
-
/** Registry stationary is out; audio has no bbox and is never a member or a singleton. */
|
|
47790
|
-
function isEligibleMember(track) {
|
|
47791
|
-
if (track.closedReason === "stationary-promotion") return false;
|
|
47792
|
-
if (track.className === "audio") return false;
|
|
47793
|
-
return true;
|
|
47794
|
-
}
|
|
47795
|
-
function diag(bbox) {
|
|
47796
|
-
return Math.hypot(bbox.w, bbox.h);
|
|
47797
|
-
}
|
|
47798
|
-
function nearestPosition(positions, t) {
|
|
47799
|
-
let best;
|
|
47800
|
-
let bestDt = 1001;
|
|
47801
|
-
for (const p of positions) {
|
|
47802
|
-
const dt = Math.abs(p.timestamp - t);
|
|
47803
|
-
if (dt <= 1e3 && dt < bestDt) {
|
|
47804
|
-
best = p;
|
|
47805
|
-
bestDt = dt;
|
|
47806
|
-
}
|
|
47807
|
-
}
|
|
47808
|
-
return best;
|
|
47809
|
-
}
|
|
47810
|
-
function meanDiag(positions) {
|
|
47811
|
-
if (positions.length === 0) return 0;
|
|
47812
|
-
let sum = 0;
|
|
47813
|
-
for (const p of positions) sum += diag(p.bbox);
|
|
47814
|
-
return sum / positions.length;
|
|
47815
|
-
}
|
|
47816
|
-
function vecLen(v) {
|
|
47817
|
-
return Math.hypot(v.x, v.y);
|
|
47818
|
-
}
|
|
47819
|
-
function cosine(a, b) {
|
|
47820
|
-
const la = vecLen(a);
|
|
47821
|
-
const lb = vecLen(b);
|
|
47822
|
-
if (la === 0 || lb === 0) return 1;
|
|
47823
|
-
return (a.x * b.x + a.y * b.y) / (la * lb);
|
|
47824
|
-
}
|
|
47825
|
-
function positionsInWindow(track, start, end) {
|
|
47826
|
-
return track.positions.filter((p) => p.timestamp >= start && p.timestamp <= end);
|
|
47827
|
-
}
|
|
47828
|
-
function windowDisplacement(positions) {
|
|
47829
|
-
const first = positions[0];
|
|
47830
|
-
const last = positions[positions.length - 1];
|
|
47831
|
-
if (!first || !last) return {
|
|
47832
|
-
x: 0,
|
|
47833
|
-
y: 0
|
|
47834
|
-
};
|
|
47835
|
-
const a = bboxCentroid(first.bbox);
|
|
47836
|
-
const b = bboxCentroid(last.bbox);
|
|
47837
|
-
return {
|
|
47838
|
-
x: b.x - a.x,
|
|
47839
|
-
y: b.y - a.y
|
|
47840
|
-
};
|
|
47841
|
-
}
|
|
47842
|
-
function windowRelativeMotion(positions) {
|
|
47843
|
-
const mean = meanDiag(positions);
|
|
47844
|
-
if (mean <= 0) return 0;
|
|
47845
|
-
return vecLen(windowDisplacement(positions)) / mean;
|
|
47846
|
-
}
|
|
47847
|
-
/**
|
|
47848
|
-
* Same cohesion + crossing veto as {@link pairTogether}, over `[now − 3 s, now]`.
|
|
47849
|
-
* Displacement/cosine use the window, not the whole track.
|
|
47850
|
-
*/
|
|
47851
|
-
function pairTogetherNow(a, b, now) {
|
|
47852
|
-
const start = now - LIVE_WINDOW_MS;
|
|
47853
|
-
const overlapStart = Math.max(a.firstSeen, b.firstSeen, start);
|
|
47854
|
-
const overlapEnd = Math.min(a.lastSeen, b.lastSeen, now);
|
|
47855
|
-
if (overlapEnd < overlapStart) return false;
|
|
47856
|
-
let joint = 0;
|
|
47857
|
-
let close = 0;
|
|
47858
|
-
for (let t = overlapStart; t <= overlapEnd; t += 500) {
|
|
47859
|
-
const pa = nearestPosition(a.positions, t);
|
|
47860
|
-
const pb = nearestPosition(b.positions, t);
|
|
47861
|
-
if (!pa || !pb) continue;
|
|
47862
|
-
joint++;
|
|
47863
|
-
const ca = bboxCentroid(pa.bbox);
|
|
47864
|
-
const cb = bboxCentroid(pb.bbox);
|
|
47865
|
-
const gap = Math.hypot(ca.x - cb.x, ca.y - cb.y);
|
|
47866
|
-
const scale = (diag(pa.bbox) + diag(pb.bbox)) / 2;
|
|
47867
|
-
if ((scale > 0 ? gap / scale : Number.POSITIVE_INFINITY) <= 1.5) close++;
|
|
47868
|
-
}
|
|
47869
|
-
if (joint === 0) return false;
|
|
47870
|
-
if (close / joint < .5) return false;
|
|
47871
|
-
const wa = positionsInWindow(a, start, now);
|
|
47872
|
-
const wb = positionsInWindow(b, start, now);
|
|
47873
|
-
const aMoves = windowRelativeMotion(wa) >= MOVE_EPS;
|
|
47874
|
-
const bMoves = windowRelativeMotion(wb) >= MOVE_EPS;
|
|
47875
|
-
if (aMoves && bMoves && cosine(windowDisplacement(wa), windowDisplacement(wb)) < .3) return false;
|
|
47876
|
-
return true;
|
|
47877
|
-
}
|
|
47878
|
-
var GroupLiveAssigner = class {
|
|
47879
|
-
nextId;
|
|
47880
|
-
snapshots = /* @__PURE__ */ new Map();
|
|
47881
|
-
groups = /* @__PURE__ */ new Map();
|
|
47882
|
-
membership = /* @__PURE__ */ new Map();
|
|
47883
|
-
/** `${trackId}>${groupId}` → first instant the join predicate held. */
|
|
47884
|
-
joinSince = /* @__PURE__ */ new Map();
|
|
47885
|
-
/** trackId → first instant they failed pair against every remaining member. */
|
|
47886
|
-
leaveSince = /* @__PURE__ */ new Map();
|
|
47887
|
-
constructor(nextId = () => randomUUID()) {
|
|
47888
|
-
this.nextId = nextId;
|
|
47889
|
-
}
|
|
47890
|
-
observe(track, now) {
|
|
47891
|
-
this.snapshots.set(track.trackId, track);
|
|
47892
|
-
if (!isEligibleMember(track)) {
|
|
47893
|
-
this.removeFromGroup(track.trackId);
|
|
47894
|
-
return;
|
|
47895
|
-
}
|
|
47896
|
-
if (!this.membership.has(track.trackId)) this.birthSingleton(track, now);
|
|
47897
|
-
this.considerLeave(track, now);
|
|
47898
|
-
const groupId = this.membership.get(track.trackId);
|
|
47899
|
-
const group = groupId !== void 0 ? this.groups.get(groupId) : void 0;
|
|
47900
|
-
if (group) for (const id of [...group.members]) {
|
|
47901
|
-
if (id === track.trackId) continue;
|
|
47902
|
-
const snap = this.snapshots.get(id);
|
|
47903
|
-
if (snap) this.considerLeave(snap, now);
|
|
47904
|
-
}
|
|
47905
|
-
this.considerJoin(track, now);
|
|
47906
|
-
}
|
|
47907
|
-
groupOf(trackId) {
|
|
47908
|
-
return this.membership.get(trackId);
|
|
47909
|
-
}
|
|
47910
|
-
snapshot(trackId) {
|
|
47911
|
-
return this.snapshots.get(trackId);
|
|
47912
|
-
}
|
|
47913
|
-
liveGroups() {
|
|
47914
|
-
return [...this.groups.values()].map((g) => this.toLive(g));
|
|
47915
|
-
}
|
|
47916
|
-
idleGroups(now, idleMs) {
|
|
47917
|
-
return this.liveGroups().filter((g) => now >= g.lastActiveAt + idleMs);
|
|
47918
|
-
}
|
|
47919
|
-
drop(ids) {
|
|
47920
|
-
for (const id of ids) {
|
|
47921
|
-
const g = this.groups.get(id);
|
|
47922
|
-
if (!g) continue;
|
|
47923
|
-
for (const member of g.members) this.membership.delete(member);
|
|
47924
|
-
this.groups.delete(id);
|
|
47925
|
-
}
|
|
47926
|
-
}
|
|
47927
|
-
birthSingleton(track, now) {
|
|
47928
|
-
const id = this.nextId();
|
|
47929
|
-
this.groups.set(id, {
|
|
47930
|
-
id,
|
|
47931
|
-
deviceId: track.deviceId,
|
|
47932
|
-
openedAt: now,
|
|
47933
|
-
members: new Set([track.trackId])
|
|
47934
|
-
});
|
|
47935
|
-
this.membership.set(track.trackId, id);
|
|
47936
|
-
}
|
|
47937
|
-
considerLeave(track, now) {
|
|
47938
|
-
const groupId = this.membership.get(track.trackId);
|
|
47939
|
-
if (groupId === void 0) return;
|
|
47940
|
-
const group = this.groups.get(groupId);
|
|
47941
|
-
if (!group || group.members.size < 2) {
|
|
47942
|
-
this.leaveSince.delete(track.trackId);
|
|
47943
|
-
return;
|
|
47944
|
-
}
|
|
47945
|
-
if ([...group.members].filter((id) => id !== track.trackId).some((id) => {
|
|
47946
|
-
const other = this.snapshots.get(id);
|
|
47947
|
-
return other !== void 0 && pairTogetherNow(track, other, now);
|
|
47948
|
-
})) {
|
|
47949
|
-
this.leaveSince.delete(track.trackId);
|
|
47950
|
-
return;
|
|
47951
|
-
}
|
|
47952
|
-
const since = this.leaveSince.get(track.trackId) ?? now;
|
|
47953
|
-
this.leaveSince.set(track.trackId, since);
|
|
47954
|
-
if (now - since < 3e3) return;
|
|
47955
|
-
this.peel(track, now);
|
|
47956
|
-
}
|
|
47957
|
-
peel(track, now) {
|
|
47958
|
-
this.removeFromGroup(track.trackId);
|
|
47959
|
-
this.leaveSince.delete(track.trackId);
|
|
47960
|
-
this.birthSingleton(track, now);
|
|
47961
|
-
}
|
|
47962
|
-
considerJoin(track, now) {
|
|
47963
|
-
const mineId = this.membership.get(track.trackId);
|
|
47964
|
-
if (mineId === void 0) return;
|
|
47965
|
-
const mine = this.groups.get(mineId);
|
|
47966
|
-
if (!mine || mine.members.size !== 1) {
|
|
47967
|
-
this.clearJoinClocks(track.trackId);
|
|
47968
|
-
return;
|
|
47969
|
-
}
|
|
47970
|
-
let best;
|
|
47971
|
-
for (const group of this.groups.values()) {
|
|
47972
|
-
if (group.id === mineId || group.deviceId !== track.deviceId) continue;
|
|
47973
|
-
let gap = Number.POSITIVE_INFINITY;
|
|
47974
|
-
let together = false;
|
|
47975
|
-
for (const memberId of group.members) {
|
|
47976
|
-
const other = this.snapshots.get(memberId);
|
|
47977
|
-
if (!other) continue;
|
|
47978
|
-
if (pairTogetherNow(track, other, now)) {
|
|
47979
|
-
together = true;
|
|
47980
|
-
gap = Math.min(gap, centroidGap(track, other, now));
|
|
47981
|
-
}
|
|
47982
|
-
}
|
|
47983
|
-
if (!together) continue;
|
|
47984
|
-
if (best === void 0 || gap < best.gap) best = {
|
|
47985
|
-
groupId: group.id,
|
|
47986
|
-
gap
|
|
47987
|
-
};
|
|
47988
|
-
}
|
|
47989
|
-
if (best === void 0) {
|
|
47990
|
-
this.clearJoinClocks(track.trackId);
|
|
47991
|
-
return;
|
|
47992
|
-
}
|
|
47993
|
-
const key = `${track.trackId}>${best.groupId}`;
|
|
47994
|
-
for (const existing of [...this.joinSince.keys()]) if (existing.startsWith(`${track.trackId}>`) && existing !== key) this.joinSince.delete(existing);
|
|
47995
|
-
const since = this.joinSince.get(key) ?? now;
|
|
47996
|
-
this.joinSince.set(key, since);
|
|
47997
|
-
if (now - since < 1500) return;
|
|
47998
|
-
this.absorb(track.trackId, mineId, best.groupId);
|
|
47999
|
-
}
|
|
48000
|
-
absorb(trackId, fromId, intoId) {
|
|
48001
|
-
const into = this.groups.get(intoId);
|
|
48002
|
-
if (!into) return;
|
|
48003
|
-
into.members.add(trackId);
|
|
48004
|
-
this.membership.set(trackId, intoId);
|
|
48005
|
-
this.groups.delete(fromId);
|
|
48006
|
-
this.clearJoinClocks(trackId);
|
|
48007
|
-
this.leaveSince.delete(trackId);
|
|
48008
|
-
}
|
|
48009
|
-
removeFromGroup(trackId) {
|
|
48010
|
-
const groupId = this.membership.get(trackId);
|
|
48011
|
-
this.membership.delete(trackId);
|
|
48012
|
-
this.clearJoinClocks(trackId);
|
|
48013
|
-
this.leaveSince.delete(trackId);
|
|
48014
|
-
if (groupId === void 0) return;
|
|
48015
|
-
const group = this.groups.get(groupId);
|
|
48016
|
-
if (!group) return;
|
|
48017
|
-
group.members.delete(trackId);
|
|
48018
|
-
if (group.members.size === 0) this.groups.delete(groupId);
|
|
48019
|
-
}
|
|
48020
|
-
clearJoinClocks(trackId) {
|
|
48021
|
-
for (const key of [...this.joinSince.keys()]) if (key.startsWith(`${trackId}>`)) this.joinSince.delete(key);
|
|
48022
|
-
}
|
|
48023
|
-
toLive(group) {
|
|
48024
|
-
let lastActiveAt = group.openedAt;
|
|
48025
|
-
for (const id of group.members) {
|
|
48026
|
-
const snap = this.snapshots.get(id);
|
|
48027
|
-
if (snap && snap.lastSeen > lastActiveAt) lastActiveAt = snap.lastSeen;
|
|
48028
|
-
}
|
|
48029
|
-
return {
|
|
48030
|
-
id: group.id,
|
|
48031
|
-
deviceId: group.deviceId,
|
|
48032
|
-
memberTrackIds: [...group.members],
|
|
48033
|
-
openedAt: group.openedAt,
|
|
48034
|
-
lastActiveAt
|
|
48035
|
-
};
|
|
48036
|
-
}
|
|
48037
|
-
};
|
|
48038
|
-
function centroidGap(a, b, now) {
|
|
48039
|
-
const pa = a.positions.reduce((best, p) => Math.abs(p.timestamp - now) < Math.abs(best.timestamp - now) ? p : best, a.positions[0]);
|
|
48040
|
-
const pb = b.positions.reduce((best, p) => Math.abs(p.timestamp - now) < Math.abs(best.timestamp - now) ? p : best, b.positions[0]);
|
|
48041
|
-
if (!pa || !pb) return Number.POSITIVE_INFINITY;
|
|
48042
|
-
const ca = bboxCentroid(pa.bbox);
|
|
48043
|
-
const cb = bboxCentroid(pb.bbox);
|
|
48044
|
-
return Math.hypot(ca.x - cb.x, ca.y - cb.y);
|
|
48045
|
-
}
|
|
48046
|
-
//#endregion
|
|
48047
|
-
//#region src/pipeline-analytics/group/group-session.ts
|
|
48048
|
-
/** Same idle as the notification burst (`NC_GROUP_DEFAULT_IDLE_SEC`). */
|
|
48049
|
-
var GROUP_SESSION_IDLE_MS = 3e4;
|
|
48050
|
-
//#endregion
|
|
48051
|
-
//#region src/pipeline-analytics/group/group-live-buffer.ts
|
|
48052
|
-
function toGroupableTrack(track) {
|
|
48053
|
-
return {
|
|
48054
|
-
trackId: track.trackId,
|
|
48055
|
-
deviceId: track.deviceId,
|
|
48056
|
-
className: track.className,
|
|
48057
|
-
firstSeen: track.firstSeen,
|
|
48058
|
-
lastSeen: track.lastSeen,
|
|
48059
|
-
positions: track.positions.map((p) => ({
|
|
48060
|
-
...p,
|
|
48061
|
-
bbox: { ...p.bbox }
|
|
48062
|
-
})),
|
|
48063
|
-
...track.closedReason !== void 0 ? { closedReason: track.closedReason } : {}
|
|
48064
|
-
};
|
|
48065
|
-
}
|
|
48066
|
-
var GroupLiveBuffer = class {
|
|
48067
|
-
assigner;
|
|
48068
|
-
primed = /* @__PURE__ */ new Set();
|
|
48069
|
-
constructor(nextId) {
|
|
48070
|
-
this.assigner = new GroupLiveAssigner(nextId);
|
|
48071
|
-
}
|
|
48072
|
-
observe(track, now) {
|
|
48073
|
-
this.assigner.observe(track, now);
|
|
48074
|
-
}
|
|
48075
|
-
groupOf(trackId) {
|
|
48076
|
-
return this.assigner.groupOf(trackId);
|
|
48077
|
-
}
|
|
48078
|
-
snapshot(trackId) {
|
|
48079
|
-
return this.assigner.snapshot(trackId);
|
|
48080
|
-
}
|
|
48081
|
-
liveGroups() {
|
|
48082
|
-
return this.assigner.liveGroups();
|
|
48083
|
-
}
|
|
48084
|
-
idleGroups(now, idleMs = GROUP_SESSION_IDLE_MS) {
|
|
48085
|
-
return this.assigner.idleGroups(now, idleMs);
|
|
48086
|
-
}
|
|
48087
|
-
drop(ids) {
|
|
48088
|
-
this.assigner.drop(ids);
|
|
48089
|
-
}
|
|
48090
|
-
isPrimed(deviceId) {
|
|
48091
|
-
return this.primed.has(deviceId);
|
|
48092
|
-
}
|
|
48093
|
-
markPrimed(deviceId) {
|
|
48094
|
-
this.primed.add(deviceId);
|
|
48095
|
-
}
|
|
48096
|
-
};
|
|
48097
|
-
function trackToLite(track) {
|
|
48098
|
-
return {
|
|
48099
|
-
trackId: track.trackId,
|
|
48100
|
-
className: track.className,
|
|
48101
|
-
classes: track.classes,
|
|
48102
|
-
firstSeen: track.firstSeen,
|
|
48103
|
-
lastSeen: track.lastSeen
|
|
48104
|
-
};
|
|
48105
|
-
}
|
|
48106
|
-
/**
|
|
48107
|
-
* Gallery and digest read the same ladder: groupCrop, then the native-frame
|
|
48108
|
-
* pointer, then a member's clean full frame. Plate / face / member
|
|
48109
|
-
* thumbnails never reach the picker — a plate read overwrites the vehicle
|
|
48110
|
-
* first-level crop under the same `thumbnail` kind. A multi with no crop
|
|
48111
|
-
* still has a picture if the sealer stored `nativeKey`.
|
|
48112
|
-
*/
|
|
48113
|
-
var DIGEST_MEMBER_FALLBACK_KINDS = new Set([
|
|
48114
|
-
"keyFrame",
|
|
48115
|
-
"keyFrameSmall",
|
|
48116
|
-
"fullFrame"
|
|
48117
|
-
]);
|
|
48118
|
-
function digestMediaRows(input) {
|
|
48119
|
-
const owned = [];
|
|
48120
|
-
if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
|
|
48121
|
-
key: input.mediaKey,
|
|
48122
|
-
kind: "groupCrop",
|
|
48123
|
-
timestamp: input.timestamp,
|
|
48124
|
-
sizeBytes: null
|
|
48125
|
-
});
|
|
48126
|
-
if (input.nativeKey !== null && input.nativeKey.length > 0) owned.push({
|
|
48127
|
-
key: input.nativeKey,
|
|
48128
|
-
kind: "nativeFrame",
|
|
48129
|
-
timestamp: input.timestamp,
|
|
48130
|
-
sizeBytes: null
|
|
48131
|
-
});
|
|
48132
|
-
if (owned.length > 0) return owned;
|
|
48133
|
-
return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
|
|
48134
|
-
}
|
|
48135
|
-
//#endregion
|
|
48136
|
-
//#region src/pipeline-analytics/group/group-crop.ts
|
|
48137
|
-
/** Always containSubject: true. Never the 0.5 delivered threshold. */
|
|
48138
|
-
function groupWideCentralSquareLayout(unionBbox, frame) {
|
|
48139
|
-
return wideCentralSquareLayout(unionBbox, frame, { containSubject: true });
|
|
48140
|
-
}
|
|
48141
|
-
async function cropGroupFromRgb(rgb, frameWidth, frameHeight, unionBbox) {
|
|
48142
|
-
const layout = groupWideCentralSquareLayout(unionBbox, {
|
|
48143
|
-
W: frameWidth,
|
|
48144
|
-
H: frameHeight
|
|
48145
|
-
});
|
|
48146
|
-
const { crop: slab } = await extractCrop(rgb, frameWidth, frameHeight, layout.fetch);
|
|
48147
|
-
return composeWideCentralSquareThumbnail(slab, layout);
|
|
48148
|
-
}
|
|
48149
|
-
//#endregion
|
|
48150
|
-
//#region src/pipeline-analytics/group/group-instant.ts
|
|
48151
|
-
/**
|
|
48152
|
-
* Chosen instant + union bbox for a group, at close, over stored positions[]
|
|
48153
|
-
* (`docs/design/2026-08-21-group-entity-and-group-media.md` §4.1).
|
|
48154
|
-
*
|
|
48155
|
-
* Tier order — the first that separates two candidates decides:
|
|
48156
|
-
* 1. completeness (members with a position within ±1 s)
|
|
48157
|
-
* 2. square containment of the union (D212)
|
|
48158
|
-
* 3. semantic (a member's confirmed recognition)
|
|
48159
|
-
* 4. edge-clear (union touches no frame border)
|
|
48160
|
-
* 5. effective score = mean(confidence) × containment × sizeFactor
|
|
48161
|
-
*
|
|
48162
|
-
* Retrospective. No new capture path.
|
|
48163
|
-
*/
|
|
48164
|
-
var GROUP_MEDIA_MATCH_MS = 2e3;
|
|
48165
|
-
function nearest(positions, t, windowMs) {
|
|
48166
|
-
let best;
|
|
48167
|
-
let bestDt = windowMs + 1;
|
|
48168
|
-
for (const p of positions) {
|
|
48169
|
-
const dt = Math.abs(p.timestamp - t);
|
|
48170
|
-
if (dt <= windowMs && dt < bestDt) {
|
|
48171
|
-
best = p;
|
|
48172
|
-
bestDt = dt;
|
|
48173
|
-
}
|
|
48174
|
-
}
|
|
48175
|
-
return best;
|
|
48176
|
-
}
|
|
48177
|
-
function unionOf(boxes) {
|
|
48178
|
-
if (boxes.length === 0) return null;
|
|
48179
|
-
let x0 = Infinity;
|
|
48180
|
-
let y0 = Infinity;
|
|
48181
|
-
let x1 = -Infinity;
|
|
48182
|
-
let y1 = -Infinity;
|
|
48183
|
-
for (const b of boxes) {
|
|
48184
|
-
x0 = Math.min(x0, b.x);
|
|
48185
|
-
y0 = Math.min(y0, b.y);
|
|
48186
|
-
x1 = Math.max(x1, b.x + b.w);
|
|
48187
|
-
y1 = Math.max(y1, b.y + b.h);
|
|
48188
|
-
}
|
|
48189
|
-
return {
|
|
48190
|
-
x: x0,
|
|
48191
|
-
y: y0,
|
|
48192
|
-
w: x1 - x0,
|
|
48193
|
-
h: y1 - y0
|
|
48194
|
-
};
|
|
48195
|
-
}
|
|
48196
|
-
function touchesBorder(box, frame, tol = 1) {
|
|
48197
|
-
return box.x <= tol || box.y <= tol || box.x + box.w >= frame.width - tol || box.y + box.h >= frame.height - tol;
|
|
48198
|
-
}
|
|
48199
|
-
function scoreInstant(members, t, frame) {
|
|
48200
|
-
const boxes = [];
|
|
48201
|
-
let semantic = false;
|
|
48202
|
-
let confSum = 0;
|
|
48203
|
-
let confN = 0;
|
|
48204
|
-
for (const m of members) {
|
|
48205
|
-
const p = nearest(m.positions, t, MATCH_WINDOW_MS);
|
|
48206
|
-
if (!p) continue;
|
|
48207
|
-
boxes.push(p.bbox);
|
|
48208
|
-
if (m.semantic === true) semantic = true;
|
|
48209
|
-
if (m.confidence !== void 0) {
|
|
48210
|
-
confSum += m.confidence;
|
|
48211
|
-
confN += 1;
|
|
48212
|
-
}
|
|
48213
|
-
}
|
|
48214
|
-
const union = unionOf(boxes);
|
|
48215
|
-
if (!union) return null;
|
|
48216
|
-
const square = centralSquareContainmentFraction(union, {
|
|
48217
|
-
W: frame.width,
|
|
48218
|
-
H: frame.height
|
|
48219
|
-
});
|
|
48220
|
-
const containment = bboxContainment(union, frame.width, frame.height);
|
|
48221
|
-
const size = bboxSizeScore(union, frame.width, frame.height);
|
|
48222
|
-
const meanConf = confN > 0 ? confSum / confN : 1;
|
|
48223
|
-
return {
|
|
48224
|
-
timestamp: t,
|
|
48225
|
-
union,
|
|
48226
|
-
visibleCount: boxes.length,
|
|
48227
|
-
square,
|
|
48228
|
-
semantic,
|
|
48229
|
-
edgeClear: !touchesBorder(union, frame),
|
|
48230
|
-
effective: meanConf * containment * size
|
|
48231
|
-
};
|
|
48232
|
-
}
|
|
48233
|
-
function better(a, b) {
|
|
48234
|
-
if (a.visibleCount !== b.visibleCount) return a.visibleCount > b.visibleCount ? a : b;
|
|
48235
|
-
if (a.square !== b.square) return a.square > b.square ? a : b;
|
|
48236
|
-
if (a.semantic !== b.semantic) return a.semantic ? a : b;
|
|
48237
|
-
if (a.edgeClear !== b.edgeClear) return a.edgeClear ? a : b;
|
|
48238
|
-
if (a.effective !== b.effective) return a.effective > b.effective ? a : b;
|
|
48239
|
-
return a.timestamp <= b.timestamp ? a : b;
|
|
48240
|
-
}
|
|
48241
|
-
function chooseGroupInstant(members, frame) {
|
|
48242
|
-
if (members.length === 0) return null;
|
|
48243
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
48244
|
-
for (const m of members) for (const p of m.positions) candidates.add(p.timestamp);
|
|
48245
|
-
let best = null;
|
|
48246
|
-
for (const t of candidates) {
|
|
48247
|
-
const scored = scoreInstant(members, t, frame);
|
|
48248
|
-
if (!scored) continue;
|
|
48249
|
-
best = best === null ? scored : better(scored, best);
|
|
48250
|
-
}
|
|
48251
|
-
if (!best) return null;
|
|
48252
|
-
const w = Math.max(frame.width, 1);
|
|
48253
|
-
const h = Math.max(frame.height, 1);
|
|
48254
|
-
return {
|
|
48255
|
-
timestamp: best.timestamp,
|
|
48256
|
-
union: best.union,
|
|
48257
|
-
ux: best.union.x / w,
|
|
48258
|
-
uy: best.union.y / h,
|
|
48259
|
-
uw: best.union.w / w,
|
|
48260
|
-
uh: best.union.h / h,
|
|
48261
|
-
visibleCount: best.visibleCount
|
|
48262
|
-
};
|
|
48263
|
-
}
|
|
48264
|
-
/** Longest-lived member that owns a clean full frame; ties break on trackId. */
|
|
48265
|
-
function chooseAnchorTrackId(members, ownsCleanFrame) {
|
|
48266
|
-
const eligible = members.filter((m) => ownsCleanFrame(m.trackId));
|
|
48267
|
-
if (eligible.length === 0) return null;
|
|
48268
|
-
return eligible.toSorted((a, b) => {
|
|
48269
|
-
const life = b.lastSeen - b.firstSeen - (a.lastSeen - a.firstSeen);
|
|
48270
|
-
return life !== 0 ? life : a.trackId.localeCompare(b.trackId);
|
|
48271
|
-
})[0].trackId;
|
|
48272
|
-
}
|
|
48273
|
-
/**
|
|
48274
|
-
* The RGB source for a group crop is a full native frame. Thumbnails are
|
|
48275
|
-
* already single-subject crops and cannot substitute for union-bbox geometry.
|
|
48276
|
-
*
|
|
48277
|
-
* Prefer a keyFrame within {@link GROUP_MEDIA_MATCH_MS} of the chosen instant;
|
|
48278
|
-
* if none, take the nearest keyFrame anyway — dropping the crop because the
|
|
48279
|
-
* best-shot was 3 s off the instant is how a recognised person left the
|
|
48280
|
-
* digest with no crop.
|
|
48281
|
-
*/
|
|
48282
|
-
function pickCleanPixelRow(rows, instantMs) {
|
|
48283
|
-
const ranked = (maxDt) => {
|
|
48284
|
-
const candidates = [];
|
|
48285
|
-
for (const row of rows) {
|
|
48286
|
-
const rank = row.kind === "keyFrame" ? 0 : row.kind === "keyFrameSmall" ? 1 : -1;
|
|
48287
|
-
if (rank < 0) continue;
|
|
48288
|
-
const dt = Math.abs(row.timestamp - instantMs);
|
|
48289
|
-
if (maxDt !== null && dt > maxDt) continue;
|
|
48290
|
-
candidates.push({
|
|
48291
|
-
key: row.key,
|
|
48292
|
-
dt,
|
|
48293
|
-
rank
|
|
48294
|
-
});
|
|
48295
|
-
}
|
|
48296
|
-
return candidates.toSorted((a, b) => a.rank - b.rank || a.dt - b.dt)[0];
|
|
48297
|
-
};
|
|
48298
|
-
const near = ranked(GROUP_MEDIA_MATCH_MS);
|
|
48299
|
-
if (near !== void 0) return {
|
|
48300
|
-
key: near.key,
|
|
48301
|
-
usedFallback: false
|
|
48302
|
-
};
|
|
48303
|
-
const any = ranked(null);
|
|
48304
|
-
if (any === void 0) return null;
|
|
48305
|
-
return {
|
|
48306
|
-
key: any.key,
|
|
48307
|
-
usedFallback: true
|
|
48308
|
-
};
|
|
48309
|
-
}
|
|
48310
|
-
//#endregion
|
|
48311
|
-
//#region src/pipeline-analytics/group/group-live-replay.ts
|
|
48312
|
-
function sliceTrackAt(track, now) {
|
|
48313
|
-
const positions = track.positions.filter((p) => p.timestamp <= now);
|
|
48314
|
-
const last = positions[positions.length - 1];
|
|
48315
|
-
return {
|
|
48316
|
-
...track,
|
|
48317
|
-
lastSeen: last?.timestamp ?? track.firstSeen,
|
|
48318
|
-
positions
|
|
48319
|
-
};
|
|
48320
|
-
}
|
|
48321
|
-
function replayInto(tracks, observer) {
|
|
48322
|
-
const events = [];
|
|
48323
|
-
const byId = /* @__PURE__ */ new Map();
|
|
48324
|
-
for (const track of tracks) {
|
|
48325
|
-
byId.set(track.trackId, track);
|
|
48326
|
-
if (track.positions.length === 0) {
|
|
48327
|
-
events.push({
|
|
48328
|
-
t: track.lastSeen,
|
|
48329
|
-
trackId: track.trackId
|
|
48330
|
-
});
|
|
48331
|
-
continue;
|
|
48332
|
-
}
|
|
48333
|
-
for (const p of track.positions) events.push({
|
|
48334
|
-
t: p.timestamp,
|
|
48335
|
-
trackId: track.trackId
|
|
48336
|
-
});
|
|
48337
|
-
}
|
|
48338
|
-
events.sort((a, b) => a.t - b.t || a.trackId.localeCompare(b.trackId));
|
|
48339
|
-
let lastKey = "";
|
|
48340
|
-
for (const e of events) {
|
|
48341
|
-
const key = `${e.trackId}@${e.t}`;
|
|
48342
|
-
if (key === lastKey) continue;
|
|
48343
|
-
lastKey = key;
|
|
48344
|
-
const track = byId.get(e.trackId);
|
|
48345
|
-
if (!track) continue;
|
|
48346
|
-
observer.observe(sliceTrackAt(track, e.t), e.t);
|
|
48347
|
-
}
|
|
48348
|
-
}
|
|
48349
|
-
function replayLive(tracks, assigner = new GroupLiveAssigner()) {
|
|
48350
|
-
replayInto(tracks, assigner);
|
|
48351
|
-
return assigner;
|
|
48352
|
-
}
|
|
48353
|
-
//#endregion
|
|
48354
|
-
//#region src/pipeline-analytics/group/group-sealer.ts
|
|
48355
|
-
var DEFAULT_FRAME = {
|
|
48356
|
-
width: 640,
|
|
48357
|
-
height: 360
|
|
48358
|
-
};
|
|
48359
|
-
var QUERY_LIMIT = 2e3;
|
|
48360
|
-
/** Crash recover: replay this camera only, on its next activity. */
|
|
48361
|
-
var GROUP_CRASH_LOOKBACK_MS = 2 * 6e4;
|
|
48362
|
-
function toInstant(t) {
|
|
48363
|
-
return {
|
|
48364
|
-
trackId: t.trackId,
|
|
48365
|
-
deviceId: t.deviceId,
|
|
48366
|
-
className: t.className,
|
|
48367
|
-
firstSeen: t.firstSeen,
|
|
48368
|
-
lastSeen: t.lastSeen,
|
|
48369
|
-
positions: t.positions,
|
|
48370
|
-
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
|
|
48371
|
-
semantic: t.hasFace === true || t.hasPlate === true,
|
|
48372
|
-
confidence: t.importance
|
|
48373
|
-
};
|
|
48374
|
-
}
|
|
48375
|
-
function memberBoxesAt(members, at, frame) {
|
|
48376
|
-
const w = Math.max(frame.width, 1);
|
|
48377
|
-
const h = Math.max(frame.height, 1);
|
|
48378
|
-
const boxes = [];
|
|
48379
|
-
for (const member of members) {
|
|
48380
|
-
let best = member.positions[0];
|
|
48381
|
-
let bestDt = Number.POSITIVE_INFINITY;
|
|
48382
|
-
for (const p of member.positions) {
|
|
48383
|
-
const dt = Math.abs(p.timestamp - at);
|
|
48384
|
-
if (dt <= 1e3 && dt < bestDt) {
|
|
48385
|
-
best = p;
|
|
48386
|
-
bestDt = dt;
|
|
48387
|
-
}
|
|
48388
|
-
}
|
|
48389
|
-
if (best === void 0 || bestDt === Number.POSITIVE_INFINITY) continue;
|
|
48390
|
-
boxes.push({
|
|
48391
|
-
bbox: {
|
|
48392
|
-
x: best.bbox.x / w,
|
|
48393
|
-
y: best.bbox.y / h,
|
|
48394
|
-
w: best.bbox.w / w,
|
|
48395
|
-
h: best.bbox.h / h
|
|
48396
|
-
},
|
|
48397
|
-
label: member.className,
|
|
48398
|
-
observedAt: best.timestamp
|
|
48399
|
-
});
|
|
48400
|
-
}
|
|
48401
|
-
return boxes;
|
|
48402
|
-
}
|
|
48403
|
-
function toSealer(track) {
|
|
48404
|
-
return {
|
|
48405
|
-
trackId: track.trackId,
|
|
48406
|
-
deviceId: track.deviceId,
|
|
48407
|
-
className: track.className,
|
|
48408
|
-
firstSeen: track.firstSeen,
|
|
48409
|
-
lastSeen: track.lastSeen,
|
|
48410
|
-
positions: track.positions,
|
|
48411
|
-
...track.closedReason !== void 0 ? { closedReason: track.closedReason } : {}
|
|
48412
|
-
};
|
|
48413
|
-
}
|
|
48414
|
-
var GroupSealer = class {
|
|
48415
|
-
deps;
|
|
48416
|
-
constructor(deps) {
|
|
48417
|
-
this.deps = deps;
|
|
48418
|
-
}
|
|
48419
|
-
get processEnabled() {
|
|
48420
|
-
return this.deps.enabled ?? false;
|
|
48421
|
-
}
|
|
48422
|
-
async flushIdle(buffer, now = this.deps.now()) {
|
|
48423
|
-
if (!this.processEnabled) return 0;
|
|
48424
|
-
let written = 0;
|
|
48425
|
-
for (const group of buffer.idleGroups(now, GROUP_SESSION_IDLE_MS)) if (await this.persistLiveGroup(group, buffer)) {
|
|
48426
|
-
buffer.drop([group.id]);
|
|
48427
|
-
written += 1;
|
|
48428
|
-
}
|
|
48429
|
-
return written;
|
|
48430
|
-
}
|
|
48431
|
-
/**
|
|
48432
|
-
* First activity on camera D after a crash: replay 2 minutes of that camera
|
|
48433
|
-
* into the live buffer, persist whoever is already idle, keep the rest in RAM.
|
|
48434
|
-
*/
|
|
48435
|
-
async recoverCamera(deviceId, buffer) {
|
|
48436
|
-
if (!this.processEnabled) return 0;
|
|
48437
|
-
if (buffer.isPrimed(deviceId)) return 0;
|
|
48438
|
-
const now = this.deps.now();
|
|
48439
|
-
try {
|
|
48440
|
-
replayInto(await this.deps.trackStore.queryHistorical({
|
|
48441
|
-
deviceId,
|
|
48442
|
-
since: now - GROUP_CRASH_LOOKBACK_MS,
|
|
48443
|
-
until: now,
|
|
48444
|
-
limit: QUERY_LIMIT,
|
|
48445
|
-
projection: "full"
|
|
48446
|
-
}), buffer);
|
|
48447
|
-
} catch (err) {
|
|
48448
|
-
this.deps.logger.warn("group sealer: lookback failed", {
|
|
48449
|
-
tags: { deviceId },
|
|
48450
|
-
meta: { error: String(err) }
|
|
48451
|
-
});
|
|
48452
|
-
} finally {
|
|
48453
|
-
buffer.markPrimed(deviceId);
|
|
48454
|
-
}
|
|
48455
|
-
return this.flushIdle(buffer, now);
|
|
48456
|
-
}
|
|
48457
|
-
/**
|
|
48458
|
-
* Seal ONE camera over a caller-owned window via live replay.
|
|
48459
|
-
*
|
|
48460
|
-
* This is how a digest recovers groups without a boot scan: the producer
|
|
48461
|
-
* already knows the window. Walking every camera for 48h at initialize()
|
|
48462
|
-
* is what wedged hub-main.
|
|
48463
|
-
*/
|
|
48464
|
-
async sealWindow(deviceId, since, until) {
|
|
48465
|
-
if (!this.processEnabled) return 0;
|
|
48466
|
-
try {
|
|
48467
|
-
const closed = await this.deps.trackStore.queryHistorical({
|
|
48468
|
-
deviceId,
|
|
48469
|
-
since,
|
|
48470
|
-
until,
|
|
48471
|
-
limit: QUERY_LIMIT,
|
|
48472
|
-
projection: "full"
|
|
48473
|
-
});
|
|
48474
|
-
if (closed.length === 0) return 0;
|
|
48475
|
-
const already = new Set((await this.deps.groupStore.membersOfTracks(closed.map((t) => t.trackId))).map((m) => m.trackId));
|
|
48476
|
-
const pending = closed.filter((t) => !already.has(t.trackId));
|
|
48477
|
-
if (pending.length === 0) return 0;
|
|
48478
|
-
const assigner = replayLive(pending);
|
|
48479
|
-
const now = this.deps.now();
|
|
48480
|
-
const byId = new Map(pending.map((t) => [t.trackId, t]));
|
|
48481
|
-
let written = 0;
|
|
48482
|
-
for (const group of assigner.idleGroups(now, GROUP_SESSION_IDLE_MS)) {
|
|
48483
|
-
const members = group.memberTrackIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
|
|
48484
|
-
if (members.length === 0) continue;
|
|
48485
|
-
try {
|
|
48486
|
-
await this.persistPartition(members, group.openedAt);
|
|
48487
|
-
written += 1;
|
|
48488
|
-
} catch (err) {
|
|
48489
|
-
this.deps.logger.warn("group sealer: persist failed", {
|
|
48490
|
-
tags: { deviceId },
|
|
48491
|
-
meta: {
|
|
48492
|
-
members: members.map((m) => m.trackId),
|
|
48493
|
-
error: String(err)
|
|
48494
|
-
}
|
|
48495
|
-
});
|
|
48496
|
-
}
|
|
48497
|
-
}
|
|
48498
|
-
return written;
|
|
48499
|
-
} catch (err) {
|
|
48500
|
-
this.deps.logger.warn("group sealer: device failed", {
|
|
48501
|
-
tags: { deviceId },
|
|
48502
|
-
meta: { error: String(err) }
|
|
48503
|
-
});
|
|
48504
|
-
return 0;
|
|
48505
|
-
}
|
|
48506
|
-
}
|
|
48507
|
-
async persistLiveGroup(group, buffer) {
|
|
48508
|
-
const deviceId = group.deviceId;
|
|
48509
|
-
const snaps = group.memberTrackIds.map((id) => buffer.snapshot(id)).filter((t) => t !== void 0);
|
|
48510
|
-
if (snaps.length === 0) return true;
|
|
48511
|
-
const already = new Set((await this.deps.groupStore.membersOfTracks(snaps.map((t) => t.trackId))).map((m) => m.trackId));
|
|
48512
|
-
const pending = snaps.filter((t) => !already.has(t.trackId)).map(toSealer);
|
|
48513
|
-
if (pending.length === 0) return true;
|
|
48514
|
-
try {
|
|
48515
|
-
await this.persistPartition(pending, group.openedAt);
|
|
48516
|
-
return true;
|
|
48517
|
-
} catch (err) {
|
|
48518
|
-
this.deps.logger.warn("group sealer: persist failed", {
|
|
48519
|
-
tags: { deviceId },
|
|
48520
|
-
meta: {
|
|
48521
|
-
members: pending.map((m) => m.trackId),
|
|
48522
|
-
error: String(err)
|
|
48523
|
-
}
|
|
48524
|
-
});
|
|
48525
|
-
return false;
|
|
48526
|
-
}
|
|
48527
|
-
}
|
|
48528
|
-
async persistPartition(members, openedAt) {
|
|
48529
|
-
const deviceId = members[0].deviceId;
|
|
48530
|
-
const frame = this.deps.frameDims(deviceId) ?? DEFAULT_FRAME;
|
|
48531
|
-
const instantMembers = members.map(toInstant);
|
|
48532
|
-
const instant = chooseGroupInstant(instantMembers, frame);
|
|
48533
|
-
const closedAt = Math.max(...members.map((m) => m.lastSeen));
|
|
48534
|
-
const timestamp = instant?.timestamp ?? closedAt;
|
|
48535
|
-
const mediaInfos = await Promise.all(members.map(async (m) => ({
|
|
48536
|
-
trackId: m.trackId,
|
|
48537
|
-
rows: await this.deps.mediaStore.listInfoByOwner("track", m.trackId)
|
|
48538
|
-
})));
|
|
48539
|
-
const ownsClean = (trackId) => {
|
|
48540
|
-
return mediaInfos.find((m) => m.trackId === trackId)?.rows.some((r) => r.kind === "keyFrame" || r.kind === "keyFrameSmall") ?? false;
|
|
48541
|
-
};
|
|
48542
|
-
const anchorTrackId = members.length >= 2 ? chooseAnchorTrackId(instantMembers, ownsClean) : null;
|
|
48543
|
-
let nativeKey = null;
|
|
48544
|
-
if (anchorTrackId !== null) {
|
|
48545
|
-
const rows = mediaInfos.find((m) => m.trackId === anchorTrackId)?.rows ?? [];
|
|
48546
|
-
nativeKey = (rows.find((r) => r.kind === "keyFrame") ?? rows.find((r) => r.kind === "keyFrameSmall"))?.key ?? null;
|
|
48547
|
-
}
|
|
48548
|
-
const draft = await this.deps.groupStore.insert({
|
|
48549
|
-
deviceId,
|
|
48550
|
-
timestamp,
|
|
48551
|
-
openedAt,
|
|
48552
|
-
closedAt,
|
|
48553
|
-
members: members.map((m) => ({
|
|
48554
|
-
trackId: m.trackId,
|
|
48555
|
-
firstSeen: m.firstSeen
|
|
48556
|
-
})),
|
|
48557
|
-
anchorTrackId,
|
|
48558
|
-
ux: instant?.ux ?? 0,
|
|
48559
|
-
uy: instant?.uy ?? 0,
|
|
48560
|
-
uw: instant?.uw ?? 0,
|
|
48561
|
-
uh: instant?.uh ?? 0,
|
|
48562
|
-
frameWidth: frame.width,
|
|
48563
|
-
frameHeight: frame.height,
|
|
48564
|
-
mediaKey: null,
|
|
48565
|
-
nativeKey
|
|
48566
|
-
});
|
|
48567
|
-
if (members.length < 2 || instant === null) return;
|
|
48568
|
-
const source = await this.pickCleanPixels(mediaInfos, timestamp, deviceId);
|
|
48569
|
-
if (source === null) {
|
|
48570
|
-
this.deps.logger.info("group sealer: no clean pixels for crop", {
|
|
48571
|
-
tags: { deviceId },
|
|
48572
|
-
meta: {
|
|
48573
|
-
groupId: draft.id,
|
|
48574
|
-
members: members.map((m) => m.trackId)
|
|
48575
|
-
}
|
|
48576
|
-
});
|
|
48577
|
-
return;
|
|
48578
|
-
}
|
|
48579
|
-
try {
|
|
48580
|
-
const raw = await sharp(source.jpeg).removeAlpha().toColorspace("srgb").raw().toBuffer({ resolveWithObject: true });
|
|
48581
|
-
if (raw.info.channels === 3) {
|
|
48582
|
-
const union = {
|
|
48583
|
-
x: instant.ux * raw.info.width,
|
|
48584
|
-
y: instant.uy * raw.info.height,
|
|
48585
|
-
w: instant.uw * raw.info.width,
|
|
48586
|
-
h: instant.uh * raw.info.height
|
|
48587
|
-
};
|
|
48588
|
-
const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
|
|
48589
|
-
const mediaKey = await this.deps.mediaStore.put({
|
|
48590
|
-
deviceId,
|
|
48591
|
-
ownerKind: "group",
|
|
48592
|
-
ownerId: draft.id,
|
|
48593
|
-
kind: "thumbnail",
|
|
48594
|
-
timestamp,
|
|
48595
|
-
data: crop
|
|
48596
|
-
});
|
|
48597
|
-
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
48598
|
-
}
|
|
48599
|
-
} catch (err) {
|
|
48600
|
-
this.deps.logger.warn("group sealer: crop failed", {
|
|
48601
|
-
tags: { deviceId },
|
|
48602
|
-
meta: {
|
|
48603
|
-
groupId: draft.id,
|
|
48604
|
-
error: String(err)
|
|
48605
|
-
}
|
|
48606
|
-
});
|
|
48607
|
-
}
|
|
48608
|
-
try {
|
|
48609
|
-
const composed = await composeGroupFrame({
|
|
48610
|
-
jpeg: source.jpeg,
|
|
48611
|
-
boxes: memberBoxesAt(instantMembers, timestamp, frame),
|
|
48612
|
-
at: timestamp,
|
|
48613
|
-
logger: this.deps.logger,
|
|
48614
|
-
deviceId,
|
|
48615
|
-
groupKey: draft.id
|
|
48616
|
-
});
|
|
48617
|
-
if (composed === null) return;
|
|
48618
|
-
await this.deps.mediaStore.put({
|
|
48619
|
-
deviceId,
|
|
48620
|
-
ownerKind: "group",
|
|
48621
|
-
ownerId: draft.id,
|
|
48622
|
-
kind: "fullFrameBoxed",
|
|
48623
|
-
timestamp,
|
|
48624
|
-
data: composed.bytes
|
|
48625
|
-
});
|
|
48626
|
-
} catch (err) {
|
|
48627
|
-
this.deps.logger.warn("group sealer: boxed frame failed", {
|
|
48628
|
-
tags: { deviceId },
|
|
48629
|
-
meta: {
|
|
48630
|
-
groupId: draft.id,
|
|
48631
|
-
error: String(err)
|
|
48632
|
-
}
|
|
48633
|
-
});
|
|
48634
|
-
}
|
|
48635
|
-
}
|
|
48636
|
-
async pickCleanPixels(mediaInfos, instantMs, deviceId) {
|
|
48637
|
-
const chosen = pickCleanPixelRow(mediaInfos.flatMap((info) => info.rows), instantMs);
|
|
48638
|
-
if (chosen === null) return null;
|
|
48639
|
-
if (chosen.usedFallback) this.deps.logger.info("group sealer: crop pixels not within 2s of instant, using nearest keyFrame", {
|
|
48640
|
-
tags: { deviceId },
|
|
48641
|
-
meta: {
|
|
48642
|
-
instantMs,
|
|
48643
|
-
mediaKey: chosen.key
|
|
48644
|
-
}
|
|
48645
|
-
});
|
|
48646
|
-
const file = await this.deps.mediaStore.getByKey(chosen.key);
|
|
48647
|
-
if (file === null) return null;
|
|
48648
|
-
return { jpeg: Buffer.from(file.base64, "base64") };
|
|
48649
|
-
}
|
|
48650
|
-
};
|
|
48651
|
-
//#endregion
|
|
48652
|
-
//#region src/pipeline-analytics/group/group-cursor.ts
|
|
48653
|
-
function encodeGroupCursor(cursor) {
|
|
48654
|
-
return Buffer.from(JSON.stringify({
|
|
48655
|
-
c: cursor.closedAt,
|
|
48656
|
-
i: cursor.groupId
|
|
48657
|
-
}), "utf8").toString("base64url");
|
|
48658
|
-
}
|
|
48659
|
-
function decodeGroupCursor(raw) {
|
|
48660
|
-
let parsed;
|
|
48661
|
-
try {
|
|
48662
|
-
parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
48663
|
-
} catch {
|
|
48664
|
-
throw new Error("listGroups: malformed cursor");
|
|
48665
|
-
}
|
|
48666
|
-
if (parsed === null || typeof parsed !== "object" || !("c" in parsed) || !("i" in parsed) || typeof parsed.c !== "number" || !Number.isFinite(parsed.c) || typeof parsed.i !== "string" || parsed.i.length === 0) throw new Error("listGroups: malformed cursor");
|
|
48667
|
-
return {
|
|
48668
|
-
closedAt: parsed.c,
|
|
48669
|
-
groupId: parsed.i
|
|
48670
|
-
};
|
|
48671
|
-
}
|
|
48672
|
-
function compareGroupDesc(a, b) {
|
|
48673
|
-
if (a.closedAt !== b.closedAt) return b.closedAt - a.closedAt;
|
|
48674
|
-
if (a.id === b.id) return 0;
|
|
48675
|
-
return a.id < b.id ? 1 : -1;
|
|
48676
|
-
}
|
|
48677
|
-
/** True when `row` sits strictly AFTER the cursor in DESC order. */
|
|
48678
|
-
function isAfterGroupCursor(row, cursor) {
|
|
48679
|
-
if (row.closedAt < cursor.closedAt) return true;
|
|
48680
|
-
return row.closedAt === cursor.closedAt && row.id < cursor.groupId;
|
|
48681
|
-
}
|
|
48682
|
-
//#endregion
|
|
48683
|
-
//#region src/pipeline-analytics/group/group-store.ts
|
|
48684
|
-
/**
|
|
48685
|
-
* The group ENTITY store — a co-moving partition of one session, with one
|
|
48686
|
-
* picture of its own (`docs/design/2026-08-21-group-entity-and-group-media.md`).
|
|
48687
|
-
*
|
|
48688
|
-
* Two tables, two questions:
|
|
48689
|
-
*
|
|
48690
|
-
* - `pipeline-analytics:track-groups` is the group row. It has N members and
|
|
48691
|
-
* no `trackId` column; it is owned by its members (owner-indirection).
|
|
48692
|
-
* - `pipeline-analytics:track-group-members` is one row per member. The column
|
|
48693
|
-
* is named exactly `trackId` so the classification derives `track-owned`.
|
|
48694
|
-
*
|
|
48695
|
-
* `anchorTrackId` and `nativeKey` are the only fields mutable after birth:
|
|
48696
|
-
* they are nulled when the anchor member is deleted alone. Everything else is
|
|
48697
|
-
* written once. Re-anchoring is deliberately not done.
|
|
48698
|
-
*
|
|
48699
|
-
* No age clock. A group dies when its last member does; a group with one live
|
|
48700
|
-
* member does not, and it never keeps a dead link.
|
|
48701
|
-
*/
|
|
48702
|
-
/**
|
|
48703
|
-
* @durable class=ledger owner=pipeline-analytics
|
|
48704
|
-
* write="one row per co-moving group, sealed at idle — never a live row.
|
|
48705
|
-
* Membership is the RAM members at persist, not a session partition. mediaKey is
|
|
48706
|
-
* the group's own crop; nativeKey is a POINTER to the anchor member's
|
|
48707
|
-
* keyFrame, nulled if that member is deleted alone. anchorTrackId and
|
|
48708
|
-
* nativeKey are the only fields rewritten after birth."
|
|
48709
|
-
* retention="NO age clock (D61). Rows leave with their last member through
|
|
48710
|
-
* TrackGroupStore.deleteByTracks: memberships first; zero survivors
|
|
48711
|
-
* deletes the group's media then the row; survivors plus a lost anchor
|
|
48712
|
-
* null the pointer. The orphan audit verifies the same via a second
|
|
48713
|
-
* resolver (groupId → any member still present)."
|
|
48714
|
-
*/
|
|
48715
|
-
var TRACK_GROUPS_COLLECTION = "pipeline-analytics:track-groups";
|
|
48716
|
-
/**
|
|
48717
|
-
* @durable class=ledger owner=pipeline-analytics
|
|
48718
|
-
* write="one row per member at group birth — id is '<groupId>:<trackId>'.
|
|
48719
|
-
* timestamp is the member's firstSeen so the orphan audit has a clock
|
|
48720
|
-
* column. Never rewritten."
|
|
48721
|
-
* retention="TRACK-OWNED and clockless (D61): the column is named exactly
|
|
48722
|
-
* trackId, so classification derives ownership and the cascade deletes
|
|
48723
|
-
* the row with its track. A group with surviving members stays."
|
|
48724
|
-
*/
|
|
48725
|
-
var TRACK_GROUP_MEMBERS_COLLECTION = "pipeline-analytics:track-group-members";
|
|
48726
|
-
var TRACK_GROUPS_COLUMNS = [
|
|
48727
|
-
{
|
|
48728
|
-
name: "id",
|
|
48729
|
-
type: "TEXT",
|
|
48730
|
-
primaryKey: true,
|
|
48731
|
-
notNull: true
|
|
48732
|
-
},
|
|
48733
|
-
{
|
|
48734
|
-
name: "deviceId",
|
|
48735
|
-
type: "INTEGER",
|
|
48736
|
-
notNull: true
|
|
48737
|
-
},
|
|
48738
|
-
{
|
|
48739
|
-
name: "timestamp",
|
|
48740
|
-
type: "INTEGER",
|
|
48741
|
-
notNull: true
|
|
48742
|
-
},
|
|
48743
|
-
{
|
|
48744
|
-
name: "openedAt",
|
|
48745
|
-
type: "INTEGER",
|
|
48746
|
-
notNull: true
|
|
48747
|
-
},
|
|
48748
|
-
{
|
|
48749
|
-
name: "closedAt",
|
|
48750
|
-
type: "INTEGER",
|
|
48751
|
-
notNull: true
|
|
48752
|
-
},
|
|
48753
|
-
{
|
|
48754
|
-
name: "memberCount",
|
|
48755
|
-
type: "INTEGER",
|
|
48756
|
-
notNull: true
|
|
48757
|
-
},
|
|
48758
|
-
{
|
|
48759
|
-
name: "anchorTrackId",
|
|
48760
|
-
type: "TEXT"
|
|
48761
|
-
},
|
|
48762
|
-
{
|
|
48763
|
-
name: "ux",
|
|
48764
|
-
type: "REAL",
|
|
48765
|
-
notNull: true
|
|
48766
|
-
},
|
|
48767
|
-
{
|
|
48768
|
-
name: "uy",
|
|
48769
|
-
type: "REAL",
|
|
48770
|
-
notNull: true
|
|
48771
|
-
},
|
|
48772
|
-
{
|
|
48773
|
-
name: "uw",
|
|
48774
|
-
type: "REAL",
|
|
48775
|
-
notNull: true
|
|
48776
|
-
},
|
|
48777
|
-
{
|
|
48778
|
-
name: "uh",
|
|
48779
|
-
type: "REAL",
|
|
48780
|
-
notNull: true
|
|
48781
|
-
},
|
|
48782
|
-
{
|
|
48783
|
-
name: "frameWidth",
|
|
48784
|
-
type: "INTEGER",
|
|
48785
|
-
notNull: true
|
|
48786
|
-
},
|
|
48787
|
-
{
|
|
48788
|
-
name: "frameHeight",
|
|
48789
|
-
type: "INTEGER",
|
|
48790
|
-
notNull: true
|
|
48791
|
-
},
|
|
48792
|
-
{
|
|
48793
|
-
name: "mediaKey",
|
|
48794
|
-
type: "TEXT"
|
|
48795
|
-
},
|
|
48796
|
-
{
|
|
48797
|
-
name: "nativeKey",
|
|
48798
|
-
type: "TEXT"
|
|
48799
|
-
},
|
|
48800
|
-
{
|
|
48801
|
-
name: "sealed",
|
|
48802
|
-
type: "BOOLEAN",
|
|
48803
|
-
notNull: true
|
|
48804
|
-
}
|
|
48805
|
-
];
|
|
48806
|
-
var TRACK_GROUP_MEMBERS_COLUMNS = [
|
|
48807
|
-
{
|
|
48808
|
-
name: "id",
|
|
48809
|
-
type: "TEXT",
|
|
48810
|
-
primaryKey: true,
|
|
48811
|
-
notNull: true
|
|
48812
|
-
},
|
|
48813
|
-
{
|
|
48814
|
-
name: "groupId",
|
|
48815
|
-
type: "TEXT",
|
|
48816
|
-
notNull: true
|
|
48817
|
-
},
|
|
48818
|
-
{
|
|
48819
|
-
name: "trackId",
|
|
48820
|
-
type: "TEXT",
|
|
48821
|
-
notNull: true
|
|
48822
|
-
},
|
|
48823
|
-
{
|
|
48824
|
-
name: "deviceId",
|
|
48825
|
-
type: "INTEGER",
|
|
48826
|
-
notNull: true
|
|
48827
|
-
},
|
|
48828
|
-
{
|
|
48829
|
-
name: "timestamp",
|
|
48830
|
-
type: "INTEGER",
|
|
48831
|
-
notNull: true
|
|
48832
|
-
}
|
|
48833
|
-
];
|
|
48834
|
-
var TRACK_GROUPS_INDEXES = [{
|
|
48835
|
-
name: "idx_track_groups_device_closed",
|
|
48836
|
-
columns: ["deviceId", "closedAt"]
|
|
48837
|
-
}, {
|
|
48838
|
-
name: "idx_track_groups_device_opened",
|
|
48839
|
-
columns: ["deviceId", "openedAt"]
|
|
48840
|
-
}];
|
|
48841
|
-
var TRACK_GROUP_MEMBERS_INDEXES = [{
|
|
48842
|
-
name: "idx_track_group_members_track",
|
|
48843
|
-
columns: ["trackId"]
|
|
48844
|
-
}, {
|
|
48845
|
-
name: "idx_track_group_members_group",
|
|
48846
|
-
columns: ["groupId"]
|
|
48847
|
-
}];
|
|
48848
|
-
function newGroupId() {
|
|
48849
|
-
return `grp-${randomUUID()}`;
|
|
48850
|
-
}
|
|
48851
|
-
function memberRowId(groupId, trackId) {
|
|
48852
|
-
return `${groupId}:${trackId}`;
|
|
48853
|
-
}
|
|
48854
|
-
var TrackGroupStore = class {
|
|
48855
|
-
store;
|
|
48856
|
-
logger;
|
|
48857
|
-
media;
|
|
48858
|
-
constructor(deps) {
|
|
48859
|
-
this.store = deps.store;
|
|
48860
|
-
this.logger = deps.logger;
|
|
48861
|
-
this.media = deps.media ?? null;
|
|
48862
|
-
}
|
|
48863
|
-
static async declare(store) {
|
|
48864
|
-
await Promise.all([store.declareCollection.mutate({
|
|
48865
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
48866
|
-
columns: [...TRACK_GROUPS_COLUMNS],
|
|
48867
|
-
indexes: [...TRACK_GROUPS_INDEXES]
|
|
48868
|
-
}), store.declareCollection.mutate({
|
|
48869
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
48870
|
-
columns: [...TRACK_GROUP_MEMBERS_COLUMNS],
|
|
48871
|
-
indexes: [...TRACK_GROUP_MEMBERS_INDEXES]
|
|
48872
|
-
})]);
|
|
48873
|
-
}
|
|
48874
|
-
async insert(input) {
|
|
48875
|
-
const id = newGroupId();
|
|
48876
|
-
const group = {
|
|
48877
|
-
id,
|
|
48878
|
-
deviceId: input.deviceId,
|
|
48879
|
-
timestamp: input.timestamp,
|
|
48880
|
-
openedAt: input.openedAt,
|
|
48881
|
-
closedAt: input.closedAt,
|
|
48882
|
-
memberCount: input.members.length,
|
|
48883
|
-
anchorTrackId: input.anchorTrackId,
|
|
48884
|
-
ux: input.ux,
|
|
48885
|
-
uy: input.uy,
|
|
48886
|
-
uw: input.uw,
|
|
48887
|
-
uh: input.uh,
|
|
48888
|
-
frameWidth: input.frameWidth,
|
|
48889
|
-
frameHeight: input.frameHeight,
|
|
48890
|
-
mediaKey: input.mediaKey,
|
|
48891
|
-
nativeKey: input.nativeKey,
|
|
48892
|
-
sealed: true
|
|
48893
|
-
};
|
|
48894
|
-
await this.store.set.mutate({
|
|
48895
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
48896
|
-
key: id,
|
|
48897
|
-
value: this.toRow(group)
|
|
48898
|
-
});
|
|
48899
|
-
for (const member of input.members) await this.store.set.mutate({
|
|
48900
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
48901
|
-
key: memberRowId(id, member.trackId),
|
|
48902
|
-
value: {
|
|
48903
|
-
groupId: id,
|
|
48904
|
-
trackId: member.trackId,
|
|
48905
|
-
deviceId: input.deviceId,
|
|
48906
|
-
timestamp: member.firstSeen
|
|
48907
|
-
}
|
|
48908
|
-
});
|
|
48909
|
-
return group;
|
|
48910
|
-
}
|
|
48911
|
-
/** The crop is written after birth; this is the only post-birth write besides the pointer null. */
|
|
48912
|
-
async setMediaKey(groupId, mediaKey) {
|
|
48913
|
-
const current = await this.get(groupId);
|
|
48914
|
-
if (current === null) return;
|
|
48915
|
-
await this.store.set.mutate({
|
|
48916
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
48917
|
-
key: groupId,
|
|
48918
|
-
value: this.toRow({
|
|
48919
|
-
...current,
|
|
48920
|
-
mediaKey
|
|
48921
|
-
})
|
|
48922
|
-
});
|
|
48923
|
-
}
|
|
48924
|
-
async get(groupId) {
|
|
48925
|
-
const raw = await this.store.get.query({
|
|
48926
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
48927
|
-
key: groupId
|
|
48928
|
-
});
|
|
48929
|
-
if (!raw) return null;
|
|
48930
|
-
return this.normalizeGroup({
|
|
48931
|
-
id: groupId,
|
|
48932
|
-
data: raw
|
|
48933
|
-
});
|
|
48934
|
-
}
|
|
48935
|
-
async listMembers(groupId) {
|
|
48936
|
-
return (await this.store.query.query({
|
|
48937
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
48938
|
-
filter: { where: { groupId } }
|
|
48939
|
-
})).map((r) => this.normalizeMember({
|
|
48940
|
-
id: r.id,
|
|
48941
|
-
data: r.data
|
|
48942
|
-
}));
|
|
48943
|
-
}
|
|
48944
|
-
/**
|
|
48945
|
-
* Membership for a page of groups in ONE query. `listGroupRecords` used to
|
|
48946
|
-
* call {@link listMembers} per row — 60 groups is 60 settings-store RPCs,
|
|
48947
|
-
* ~20 s, and the viewer feed times out into a retry loop.
|
|
48948
|
-
*/
|
|
48949
|
-
async listMembersForGroups(groupIds) {
|
|
48950
|
-
if (groupIds.length === 0) return [];
|
|
48951
|
-
return (await this.store.query.query({
|
|
48952
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
48953
|
-
filter: {
|
|
48954
|
-
whereIn: { groupId: [...groupIds] },
|
|
48955
|
-
limit: 1e4
|
|
48956
|
-
}
|
|
48957
|
-
})).map((r) => this.normalizeMember({
|
|
48958
|
-
id: r.id,
|
|
48959
|
-
data: r.data
|
|
48960
|
-
}));
|
|
48961
|
-
}
|
|
48962
|
-
async membersOfTracks(trackIds) {
|
|
48963
|
-
const out = [];
|
|
48964
|
-
for (const trackId of trackIds) {
|
|
48965
|
-
const rows = await this.store.query.query({
|
|
48966
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
48967
|
-
filter: { where: { trackId } }
|
|
48968
|
-
});
|
|
48969
|
-
for (const r of rows) out.push(this.normalizeMember({
|
|
48970
|
-
id: r.id,
|
|
48971
|
-
data: r.data
|
|
48972
|
-
}));
|
|
48973
|
-
}
|
|
48974
|
-
return out;
|
|
48975
|
-
}
|
|
48976
|
-
/**
|
|
48977
|
-
* Newest-closed page for the Groups feed.
|
|
48978
|
-
*
|
|
48979
|
-
* Time filter is overlap (`closedAt >= since` and `openedAt <= until`),
|
|
48980
|
-
* same as {@link listOverlapping}. Pagination is applied AFTER the window
|
|
48981
|
-
* so a day cursor is honest rather than a raw-row offset that skips live
|
|
48982
|
-
* groups on a busy camera. `deviceIds: []` is an empty page (same as
|
|
48983
|
-
* `listRecentTracks`); omit the field for every camera. `deviceId` stays
|
|
48984
|
-
* as the single-camera alias the custom-action bridge still sends.
|
|
48985
|
-
*/
|
|
48986
|
-
async listRecent(input) {
|
|
48987
|
-
const limit = Math.min(Math.max(input.limit ?? 40, 1), 100);
|
|
48988
|
-
const offset = Math.max(input.offset ?? 0, 0);
|
|
48989
|
-
const deviceIds = input.deviceIds !== void 0 ? input.deviceIds : input.deviceId !== void 0 ? [input.deviceId] : void 0;
|
|
48990
|
-
if (deviceIds !== void 0 && deviceIds.length === 0) return {
|
|
48991
|
-
groups: [],
|
|
48992
|
-
hasMore: false,
|
|
48993
|
-
nextCursor: null
|
|
48994
|
-
};
|
|
48995
|
-
const cursor = input.cursor !== void 0 && input.cursor.length > 0 ? decodeGroupCursor(input.cursor) : null;
|
|
48996
|
-
const filtered = (await this.store.query.query({
|
|
48997
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
48998
|
-
filter: {
|
|
48999
|
-
...deviceIds !== void 0 && deviceIds.length === 1 ? { where: { deviceId: deviceIds[0] } } : deviceIds !== void 0 ? { whereIn: { deviceId: [...deviceIds] } } : {},
|
|
49000
|
-
orderBy: {
|
|
49001
|
-
field: "closedAt",
|
|
49002
|
-
direction: "desc"
|
|
49003
|
-
},
|
|
49004
|
-
limit: 2e3
|
|
49005
|
-
}
|
|
49006
|
-
})).map((r) => this.normalizeGroup({
|
|
49007
|
-
id: r.id,
|
|
49008
|
-
data: r.data
|
|
49009
|
-
})).filter((g) => {
|
|
49010
|
-
if (input.sinceMs !== void 0 && g.closedAt < input.sinceMs) return false;
|
|
49011
|
-
if (input.untilMs !== void 0 && g.openedAt > input.untilMs) return false;
|
|
49012
|
-
if (cursor !== null && !isAfterGroupCursor(g, cursor)) return false;
|
|
49013
|
-
return true;
|
|
49014
|
-
}).toSorted(compareGroupDesc);
|
|
49015
|
-
const slice = (cursor !== null ? filtered : filtered.slice(offset)).slice(0, limit + 1);
|
|
49016
|
-
const groups = slice.slice(0, limit);
|
|
49017
|
-
const hasMore = slice.length > limit;
|
|
49018
|
-
const last = groups[groups.length - 1];
|
|
49019
|
-
return {
|
|
49020
|
-
groups,
|
|
49021
|
-
hasMore,
|
|
49022
|
-
nextCursor: hasMore && last !== void 0 ? encodeGroupCursor({
|
|
49023
|
-
closedAt: last.closedAt,
|
|
49024
|
-
groupId: last.id
|
|
49025
|
-
}) : null
|
|
49026
|
-
};
|
|
49027
|
-
}
|
|
49028
|
-
/** Groups whose life overlaps `[since, until]` on one camera. */
|
|
49029
|
-
async listOverlapping(input) {
|
|
49030
|
-
return (await this.store.query.query({
|
|
49031
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
49032
|
-
filter: {
|
|
49033
|
-
where: { deviceId: input.deviceId },
|
|
49034
|
-
orderBy: {
|
|
49035
|
-
field: "closedAt",
|
|
49036
|
-
direction: "desc"
|
|
49037
|
-
},
|
|
49038
|
-
limit: input.limit ?? 2e3
|
|
49039
|
-
}
|
|
49040
|
-
})).map((r) => this.normalizeGroup({
|
|
49041
|
-
id: r.id,
|
|
49042
|
-
data: r.data
|
|
49043
|
-
})).filter((g) => g.closedAt >= input.sinceMs && g.openedAt <= input.untilMs);
|
|
49044
|
-
}
|
|
49045
|
-
async existingGroupIds(ids) {
|
|
49046
|
-
const live = /* @__PURE__ */ new Set();
|
|
49047
|
-
for (const id of ids) if (await this.store.get.query({
|
|
49048
|
-
collection: "pipeline-analytics:track-groups",
|
|
49049
|
-
key: id
|
|
49050
|
-
})) live.add(id);
|
|
49051
|
-
return live;
|
|
49052
|
-
}
|
|
49053
|
-
/** Group ids that still have at least one membership row. */
|
|
49054
|
-
async groupsWithMembers(ids) {
|
|
49055
|
-
const live = /* @__PURE__ */ new Set();
|
|
49056
|
-
for (const groupId of ids) if ((await this.store.query.query({
|
|
49057
|
-
collection: "pipeline-analytics:track-group-members",
|
|
49058
|
-
filter: {
|
|
49059
|
-
where: { groupId },
|
|
49060
|
-
limit: 1
|
|
49061
|
-
}
|
|
49062
|
-
})).length > 0) live.add(groupId);
|
|
49063
|
-
return live;
|
|
49064
|
-
}
|
|
49065
|
-
/**
|
|
49066
|
-
* Cascade leaf: delete memberships for the batch; a group with zero survivors
|
|
49067
|
-
* dies (media then row); a group that lost only its anchor nulls the pointer.
|
|
49068
|
-
*/
|
|
49069
|
-
async deleteByTracks(trackIds) {
|
|
49070
|
-
const batch = new Set(trackIds);
|
|
49071
|
-
let memberRows = 0;
|
|
49072
|
-
let groupRows = 0;
|
|
49073
|
-
let mediaRows = 0;
|
|
49074
|
-
let mediaBytes = 0;
|
|
49075
|
-
const affected = /* @__PURE__ */ new Set();
|
|
49076
|
-
try {
|
|
49077
|
-
const memberships = await this.membersOfTracks(trackIds);
|
|
49078
|
-
for (const member of memberships) try {
|
|
49079
|
-
await this.store.delete.mutate({
|
|
49080
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
49081
|
-
key: member.id
|
|
49082
|
-
});
|
|
49083
|
-
memberRows += 1;
|
|
49084
|
-
affected.add(member.groupId);
|
|
49085
|
-
} catch (err) {
|
|
49086
|
-
this.logger.warn("TrackGroupStore.deleteByTracks member delete failed", { meta: {
|
|
49087
|
-
memberId: member.id,
|
|
49088
|
-
error: String(err)
|
|
49089
|
-
} });
|
|
49090
|
-
}
|
|
49091
|
-
for (const groupId of affected) try {
|
|
49092
|
-
if ((await this.listMembers(groupId)).length === 0) {
|
|
49093
|
-
if (this.media) {
|
|
49094
|
-
const gone = await this.media.deleteByGroups([groupId]);
|
|
49095
|
-
mediaRows += gone.rows;
|
|
49096
|
-
mediaBytes += gone.bytes;
|
|
49097
|
-
}
|
|
49098
|
-
await this.store.delete.mutate({
|
|
49099
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
49100
|
-
key: groupId
|
|
49101
|
-
});
|
|
49102
|
-
groupRows += 1;
|
|
49103
|
-
continue;
|
|
49104
|
-
}
|
|
49105
|
-
const group = await this.get(groupId);
|
|
49106
|
-
if (group?.anchorTrackId && batch.has(group.anchorTrackId)) await this.store.set.mutate({
|
|
49107
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
49108
|
-
key: groupId,
|
|
49109
|
-
value: this.toRow({
|
|
49110
|
-
...group,
|
|
49111
|
-
anchorTrackId: null,
|
|
49112
|
-
nativeKey: null
|
|
49113
|
-
})
|
|
49114
|
-
});
|
|
49115
|
-
} catch (err) {
|
|
49116
|
-
this.logger.warn("TrackGroupStore.deleteByTracks group update failed", { meta: {
|
|
49117
|
-
groupId,
|
|
49118
|
-
error: String(err)
|
|
49119
|
-
} });
|
|
49120
|
-
}
|
|
49121
|
-
} catch (err) {
|
|
49122
|
-
this.logger.warn("TrackGroupStore.deleteByTracks failed", { meta: { error: String(err) } });
|
|
49123
|
-
}
|
|
49124
|
-
return [{
|
|
49125
|
-
family: "groups",
|
|
49126
|
-
rows: memberRows + groupRows,
|
|
49127
|
-
bytes: 0
|
|
49128
|
-
}, {
|
|
49129
|
-
family: "media",
|
|
49130
|
-
rows: mediaRows,
|
|
49131
|
-
bytes: mediaBytes
|
|
49132
|
-
}];
|
|
49133
|
-
}
|
|
49134
|
-
toRow(group) {
|
|
49135
|
-
return {
|
|
49136
|
-
deviceId: group.deviceId,
|
|
49137
|
-
timestamp: group.timestamp,
|
|
49138
|
-
openedAt: group.openedAt,
|
|
49139
|
-
closedAt: group.closedAt,
|
|
49140
|
-
memberCount: group.memberCount,
|
|
49141
|
-
anchorTrackId: group.anchorTrackId,
|
|
49142
|
-
ux: group.ux,
|
|
49143
|
-
uy: group.uy,
|
|
49144
|
-
uw: group.uw,
|
|
49145
|
-
uh: group.uh,
|
|
49146
|
-
frameWidth: group.frameWidth,
|
|
49147
|
-
frameHeight: group.frameHeight,
|
|
49148
|
-
mediaKey: group.mediaKey,
|
|
49149
|
-
nativeKey: group.nativeKey,
|
|
49150
|
-
sealed: group.sealed
|
|
49151
|
-
};
|
|
49152
|
-
}
|
|
49153
|
-
normalizeGroup(r) {
|
|
49154
|
-
const data = r.data;
|
|
49155
|
-
return {
|
|
49156
|
-
id: r.id,
|
|
49157
|
-
deviceId: Number(data["deviceId"]),
|
|
49158
|
-
timestamp: Number(data["timestamp"]),
|
|
49159
|
-
openedAt: Number(data["openedAt"]),
|
|
49160
|
-
closedAt: Number(data["closedAt"]),
|
|
49161
|
-
memberCount: Number(data["memberCount"]),
|
|
49162
|
-
anchorTrackId: typeof data["anchorTrackId"] === "string" ? data["anchorTrackId"] : null,
|
|
49163
|
-
ux: Number(data["ux"]),
|
|
49164
|
-
uy: Number(data["uy"]),
|
|
49165
|
-
uw: Number(data["uw"]),
|
|
49166
|
-
uh: Number(data["uh"]),
|
|
49167
|
-
frameWidth: Number(data["frameWidth"]),
|
|
49168
|
-
frameHeight: Number(data["frameHeight"]),
|
|
49169
|
-
mediaKey: typeof data["mediaKey"] === "string" ? data["mediaKey"] : null,
|
|
49170
|
-
nativeKey: typeof data["nativeKey"] === "string" ? data["nativeKey"] : null,
|
|
49171
|
-
sealed: Boolean(data["sealed"])
|
|
49172
|
-
};
|
|
49173
|
-
}
|
|
49174
|
-
normalizeMember(r) {
|
|
49175
|
-
const data = r.data;
|
|
49176
|
-
return {
|
|
49177
|
-
id: r.id,
|
|
49178
|
-
groupId: String(data["groupId"]),
|
|
49179
|
-
trackId: String(data["trackId"]),
|
|
49180
|
-
deviceId: Number(data["deviceId"]),
|
|
49181
|
-
timestamp: Number(data["timestamp"])
|
|
49182
|
-
};
|
|
49183
|
-
}
|
|
49184
|
-
};
|
|
49185
47774
|
//#endregion
|
|
49186
47775
|
//#region src/pipeline-analytics/media-capture-log.ts
|
|
49187
47776
|
var DEFAULT_FLUSH_INTERVAL_MS$1 = 6e4;
|
|
@@ -57588,24 +56177,12 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
57588
56177
|
collection: PLATES_COLLECTION,
|
|
57589
56178
|
classification: derived(PLATE_COLUMNS)
|
|
57590
56179
|
},
|
|
57591
|
-
{
|
|
57592
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
57593
|
-
classification: derived(TRACK_GROUP_MEMBERS_COLUMNS)
|
|
57594
|
-
},
|
|
57595
56180
|
{
|
|
57596
56181
|
collection: MEDIA_COLLECTION,
|
|
57597
56182
|
classification: {
|
|
57598
56183
|
kind: "owner-indirection",
|
|
57599
56184
|
derivation: "explicit",
|
|
57600
|
-
why: "(ownerKind, ownerId) means a different entity per kind — a trackId for 'track', a PREFIXED trackId for 'face'/'plate', an eventId for 'event',
|
|
57601
|
-
}
|
|
57602
|
-
},
|
|
57603
|
-
{
|
|
57604
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
57605
|
-
classification: {
|
|
57606
|
-
kind: "owner-indirection",
|
|
57607
|
-
derivation: "explicit",
|
|
57608
|
-
why: "a group has N members and no trackId column; it is owned by its membership rows. The mapping is `resolveGroupOwner` in orphan-audit.ts (groupId → any member still present). Do not classify never-orphaned — that would make the last-member guarantee unverifiable."
|
|
56185
|
+
why: "(ownerKind, ownerId) means a different entity per kind — a trackId for 'track', a PREFIXED trackId for 'face'/'plate', an eventId for 'event', and an enrolled gallery row for 'identity'/'vehicle' that retention must never touch. A RETIRED ownerKind (the deleted group entity) resolves to an owner that can never exist again and is always an orphan. The mapping is `resolveMediaOwner` in orphan-audit.ts."
|
|
57609
56186
|
}
|
|
57610
56187
|
},
|
|
57611
56188
|
{
|
|
@@ -57717,7 +56294,7 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
57717
56294
|
collection: SCENE_LATCH_COLLECTION,
|
|
57718
56295
|
classification: {
|
|
57719
56296
|
kind: "never-orphaned",
|
|
57720
|
-
why: "it IS owned — by the scene set — but the owner-cleanup is self-contained: `pruneExcept(the scene set)` runs on every scene reload, so a latch whose scene is gone is collected there and never by retention. Deliberately NOT `owner-indirection`: that kind puts a collection into `auditableCollections()`, and the orphan audit resolves only the indirections it has a resolver for (`pipeline-analytics:media`
|
|
56297
|
+
why: "it IS owned — by the scene set — but the owner-cleanup is self-contained: `pruneExcept(the scene set)` runs on every scene reload, so a latch whose scene is gone is collected there and never by retention. Deliberately NOT `owner-indirection`: that kind puts a collection into `auditableCollections()`, and the orphan audit resolves only the indirections it has a resolver for (`pipeline-analytics:media`)."
|
|
57721
56298
|
}
|
|
57722
56299
|
},
|
|
57723
56300
|
{
|
|
@@ -57777,6 +56354,10 @@ function resolveMediaOwner(row) {
|
|
|
57777
56354
|
kind: "unresolvable",
|
|
57778
56355
|
why: "media row carries no (ownerKind, ownerId)"
|
|
57779
56356
|
};
|
|
56357
|
+
if (isRetiredMediaOwnerKind(kindRaw)) return {
|
|
56358
|
+
kind: "retired-owner",
|
|
56359
|
+
ownerKind: kindRaw
|
|
56360
|
+
};
|
|
57780
56361
|
const ownerKind = kindRaw;
|
|
57781
56362
|
if (isRetentionExemptOwnerKind(ownerKind)) return {
|
|
57782
56363
|
kind: "exempt",
|
|
@@ -57805,10 +56386,6 @@ function resolveMediaOwner(row) {
|
|
|
57805
56386
|
kind: "unresolvable",
|
|
57806
56387
|
why: `plate-owned media whose ownerId does not carry the '${PLATE_MEDIA_OWNER_PREFIX}' prefix`
|
|
57807
56388
|
};
|
|
57808
|
-
case "group": return {
|
|
57809
|
-
kind: "group",
|
|
57810
|
-
groupId: ownerId
|
|
57811
|
-
};
|
|
57812
56389
|
default: return {
|
|
57813
56390
|
kind: "unresolvable",
|
|
57814
56391
|
why: `unknown ownerKind '${kindRaw}'`
|
|
@@ -57820,13 +56397,6 @@ function resolveMediaOwner(row) {
|
|
|
57820
56397
|
* is derived the same way the classification is — an enrolled row is out of
|
|
57821
56398
|
* reach regardless of what happened to the track that produced it.
|
|
57822
56399
|
*/
|
|
57823
|
-
/** A group row lives iff any membership row still names it. */
|
|
57824
|
-
function resolveGroupOwner(row) {
|
|
57825
|
-
return {
|
|
57826
|
-
kind: "group-members",
|
|
57827
|
-
groupId: row.key
|
|
57828
|
-
};
|
|
57829
|
-
}
|
|
57830
56400
|
function resolveDeclaredTrackOwner(row, enrolmentExempt) {
|
|
57831
56401
|
if (enrolmentExempt && row.data["assigned"] === true) return {
|
|
57832
56402
|
kind: "exempt",
|
|
@@ -57857,17 +56427,6 @@ function buildOrphanScopes(collections = auditableCollections()) {
|
|
|
57857
56427
|
});
|
|
57858
56428
|
continue;
|
|
57859
56429
|
}
|
|
57860
|
-
if (entry.collection === "pipeline-analytics:track-groups") {
|
|
57861
|
-
scopes.push({
|
|
57862
|
-
id: entry.collection,
|
|
57863
|
-
collection: entry.collection,
|
|
57864
|
-
resolve: resolveGroupOwner,
|
|
57865
|
-
timestampField: "timestamp",
|
|
57866
|
-
sizeField: null,
|
|
57867
|
-
deviceIdField: "deviceId"
|
|
57868
|
-
});
|
|
57869
|
-
continue;
|
|
57870
|
-
}
|
|
57871
56430
|
continue;
|
|
57872
56431
|
}
|
|
57873
56432
|
if (entry.classification.kind !== "track-owned") continue;
|
|
@@ -57899,17 +56458,11 @@ var DEFAULT_ORPHAN_LIMITS = {
|
|
|
57899
56458
|
function questionsFor(refs) {
|
|
57900
56459
|
const trackIds = /* @__PURE__ */ new Set();
|
|
57901
56460
|
const eventIds = /* @__PURE__ */ new Set();
|
|
57902
|
-
const groupIds = /* @__PURE__ */ new Set();
|
|
57903
|
-
const groupMemberIds = /* @__PURE__ */ new Set();
|
|
57904
56461
|
for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
|
|
57905
56462
|
else if (ref.kind === "event") eventIds.add(ref.eventId);
|
|
57906
|
-
else if (ref.kind === "group") groupIds.add(ref.groupId);
|
|
57907
|
-
else if (ref.kind === "group-members") groupMemberIds.add(ref.groupId);
|
|
57908
56463
|
return {
|
|
57909
56464
|
trackIds: [...trackIds],
|
|
57910
|
-
eventIds: [...eventIds]
|
|
57911
|
-
groupIds: [...groupIds],
|
|
57912
|
-
groupMemberIds: [...groupMemberIds]
|
|
56465
|
+
eventIds: [...eventIds]
|
|
57913
56466
|
};
|
|
57914
56467
|
}
|
|
57915
56468
|
/**
|
|
@@ -57941,16 +56494,12 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
57941
56494
|
break;
|
|
57942
56495
|
}
|
|
57943
56496
|
const refs = rows.map((row) => scope.resolve(row));
|
|
57944
|
-
const { trackIds, eventIds
|
|
56497
|
+
const { trackIds, eventIds } = questionsFor(refs);
|
|
57945
56498
|
let liveTracks;
|
|
57946
56499
|
let liveEvents;
|
|
57947
|
-
let liveGroups;
|
|
57948
|
-
let liveGroupsWithMembers;
|
|
57949
56500
|
try {
|
|
57950
56501
|
liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
|
|
57951
56502
|
liveEvents = eventIds.length > 0 ? await deps.liveEventIds(eventIds) : /* @__PURE__ */ new Set();
|
|
57952
|
-
liveGroups = groupIds.length > 0 && deps.liveGroupIds ? await deps.liveGroupIds(groupIds) : /* @__PURE__ */ new Set();
|
|
57953
|
-
liveGroupsWithMembers = groupMemberIds.length > 0 && deps.liveGroupsWithMembers ? await deps.liveGroupsWithMembers(groupMemberIds) : /* @__PURE__ */ new Set();
|
|
57954
56503
|
} catch (err) {
|
|
57955
56504
|
deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
|
|
57956
56505
|
collection: scope.collection,
|
|
@@ -57974,7 +56523,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
57974
56523
|
tally.unresolvable += 1;
|
|
57975
56524
|
continue;
|
|
57976
56525
|
}
|
|
57977
|
-
if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) :
|
|
56526
|
+
if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) : false) continue;
|
|
57978
56527
|
const ts = num(row.data, scope.timestampField);
|
|
57979
56528
|
if (ts !== null && ts > graceFloor) {
|
|
57980
56529
|
tally.withinGrace += 1;
|
|
@@ -64812,8 +63361,7 @@ var customActions = {
|
|
|
64812
63361
|
...locationDiscardActions,
|
|
64813
63362
|
...viewerSettingsActions,
|
|
64814
63363
|
...photoEnrollActions,
|
|
64815
|
-
...faceRescoreActions
|
|
64816
|
-
...groupActions
|
|
63364
|
+
...faceRescoreActions
|
|
64817
63365
|
};
|
|
64818
63366
|
/**
|
|
64819
63367
|
* Assist threshold when the caller does not pin one. Packages have their own
|
|
@@ -65105,11 +63653,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
65105
63653
|
faceStore = null;
|
|
65106
63654
|
faceRecognizer = null;
|
|
65107
63655
|
plateStore = null;
|
|
65108
|
-
groupStore = null;
|
|
65109
|
-
groupSealer = null;
|
|
65110
|
-
groupLive = null;
|
|
65111
|
-
/** Serialise first-activity lookback per camera, then observe in order. */
|
|
65112
|
-
groupObserveTail = /* @__PURE__ */ new Map();
|
|
65113
63656
|
plateRecognizer = null;
|
|
65114
63657
|
objectEmbeddingStore = null;
|
|
65115
63658
|
/** Live state of the detached embedding rebuild — see `rebuildObjectEmbeddings`. */
|
|
@@ -66067,8 +64610,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66067
64610
|
...locationDiscardActions,
|
|
66068
64611
|
...viewerSettingsActions,
|
|
66069
64612
|
...photoEnrollActions,
|
|
66070
|
-
...faceRescoreActions
|
|
66071
|
-
...groupActions
|
|
64613
|
+
...faceRescoreActions
|
|
66072
64614
|
},
|
|
66073
64615
|
actionHandlers: {
|
|
66074
64616
|
...ncHandlers,
|
|
@@ -66076,7 +64618,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66076
64618
|
...this.buildDebugActionHandlers(),
|
|
66077
64619
|
...this.buildPhotoEnrollActionHandlers(),
|
|
66078
64620
|
...this.buildFaceRescoreActionHandlers(),
|
|
66079
|
-
...this.buildGroupActionHandlers(),
|
|
66080
64621
|
...this.viewerSettingsSnapshots ? makeViewerSettingsActionHandlers(this.viewerSettingsSnapshots) : {}
|
|
66081
64622
|
}
|
|
66082
64623
|
} : {}
|
|
@@ -66310,7 +64851,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66310
64851
|
SceneStore.declare(api.settingsStore),
|
|
66311
64852
|
NotificationCenter.declare(api.settingsStore),
|
|
66312
64853
|
ViewerSettingsSnapshotStore.declare(api.settingsStore),
|
|
66313
|
-
TrackGroupStore.declare(api.settingsStore),
|
|
66314
64854
|
declareMigrationMarkers(api.settingsStore)
|
|
66315
64855
|
]);
|
|
66316
64856
|
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
@@ -66670,31 +65210,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66670
65210
|
logger: logger.child("PlateStore")
|
|
66671
65211
|
});
|
|
66672
65212
|
this.plateStore = plateStore;
|
|
66673
|
-
const vehicleStore = new VehicleStore({
|
|
66674
|
-
store: api.settingsStore,
|
|
66675
|
-
logger: logger.child("VehicleStore")
|
|
66676
|
-
});
|
|
66677
|
-
const groupStore = new TrackGroupStore({
|
|
66678
|
-
store: api.settingsStore,
|
|
66679
|
-
logger: logger.child("TrackGroupStore"),
|
|
66680
|
-
media: mediaStore
|
|
66681
|
-
});
|
|
66682
|
-
this.groupStore = groupStore;
|
|
66683
|
-
this.groupLive = new GroupLiveBuffer();
|
|
66684
|
-
this.groupSealer = new GroupSealer({
|
|
66685
|
-
trackStore,
|
|
66686
|
-
groupStore,
|
|
66687
|
-
mediaStore,
|
|
66688
|
-
logger: logger.child("GroupSealer"),
|
|
66689
|
-
frameDims: (deviceId) => {
|
|
66690
|
-
const dims = this.lastFrameDimsByDevice.get(deviceId);
|
|
66691
|
-
return dims ? {
|
|
66692
|
-
width: dims.w,
|
|
66693
|
-
height: dims.h
|
|
66694
|
-
} : void 0;
|
|
66695
|
-
},
|
|
66696
|
-
now: () => Date.now()
|
|
66697
|
-
});
|
|
66698
65213
|
return {
|
|
66699
65214
|
trackStore,
|
|
66700
65215
|
mediaStore,
|
|
@@ -66702,8 +65217,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66702
65217
|
identityStore,
|
|
66703
65218
|
faceStore,
|
|
66704
65219
|
plateStore,
|
|
66705
|
-
vehicleStore
|
|
66706
|
-
|
|
65220
|
+
vehicleStore: new VehicleStore({
|
|
65221
|
+
store: api.settingsStore,
|
|
65222
|
+
logger: logger.child("VehicleStore")
|
|
65223
|
+
})
|
|
66707
65224
|
};
|
|
66708
65225
|
}
|
|
66709
65226
|
/** Face + plate recognizers, the durable keyFrame-derived plate crop and the
|
|
@@ -67224,28 +65741,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
67224
65741
|
}));
|
|
67225
65742
|
},
|
|
67226
65743
|
listMediaInfo: async (id) => {
|
|
67227
|
-
const group = await stores.groupStore.get(id);
|
|
67228
|
-
if (group) {
|
|
67229
|
-
if (group.mediaKey || group.nativeKey) return digestMediaRows({
|
|
67230
|
-
mediaKey: group.mediaKey,
|
|
67231
|
-
nativeKey: group.nativeKey,
|
|
67232
|
-
timestamp: group.timestamp,
|
|
67233
|
-
memberRows: []
|
|
67234
|
-
});
|
|
67235
|
-
const members = await stores.groupStore.listMembers(group.id);
|
|
67236
|
-
const memberRows = (await Promise.all(members.map((m) => stores.mediaStore.listInfoByOwner("track", m.trackId)))).flatMap((rows) => rows.map((r) => ({
|
|
67237
|
-
key: r.key,
|
|
67238
|
-
kind: r.kind,
|
|
67239
|
-
timestamp: r.timestamp,
|
|
67240
|
-
sizeBytes: r.sizeBytes
|
|
67241
|
-
})));
|
|
67242
|
-
return digestMediaRows({
|
|
67243
|
-
mediaKey: group.mediaKey,
|
|
67244
|
-
nativeKey: group.nativeKey,
|
|
67245
|
-
timestamp: group.timestamp,
|
|
67246
|
-
memberRows
|
|
67247
|
-
});
|
|
67248
|
-
}
|
|
67249
65744
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
67250
65745
|
key: r.key,
|
|
67251
65746
|
kind: r.kind,
|
|
@@ -68616,7 +67111,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
68616
67111
|
state: t.state
|
|
68617
67112
|
});
|
|
68618
67113
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
68619
|
-
this.enqueueLiveGroup(upserted, result.timestamp);
|
|
68620
67114
|
}
|
|
68621
67115
|
if (result.rawTrackedDetections.length > 0) {
|
|
68622
67116
|
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
@@ -70713,11 +69207,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
70713
69207
|
async sweepExpiredTracks() {
|
|
70714
69208
|
if (this.shuttingDown || !this.trackStore) return;
|
|
70715
69209
|
await this.trackCloser.sweep();
|
|
70716
|
-
if (this.isPostProcessingNode && this.groupLive) this.groupSealer?.flushIdle(this.groupLive);
|
|
70717
69210
|
}
|
|
70718
|
-
/** Hub-only: assign this sighting in RAM. First activity on a camera
|
|
70719
|
-
* replays 2 minutes of that camera (crash recover), then observe is sync. */
|
|
70720
|
-
enqueueLiveGroup(track, now) {}
|
|
70721
69211
|
/** Unified retention (Phase 1): earliest retained footage per device — the
|
|
70722
69212
|
* follow-recordings cutoff source. Cached 10 min: availability answers in
|
|
70723
69213
|
* ~70 ms but the sweep asks once per device per pass. `null` = no footage
|
|
@@ -71419,16 +69909,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71419
69909
|
};
|
|
71420
69910
|
if (plausible) this.stationaryRegistry?.promote(entry);
|
|
71421
69911
|
processor.dropTrack(track.trackId);
|
|
71422
|
-
const peek = this.trackStore?.peekActive(track.trackId);
|
|
71423
|
-
if (peek) this.enqueueLiveGroup(toGroupableTrack({
|
|
71424
|
-
trackId: track.trackId,
|
|
71425
|
-
deviceId,
|
|
71426
|
-
className: track.className,
|
|
71427
|
-
firstSeen: peek.firstSeen,
|
|
71428
|
-
lastSeen: timestamp,
|
|
71429
|
-
positions: peek.positions,
|
|
71430
|
-
closedReason: "stationary-promotion"
|
|
71431
|
-
}), timestamp);
|
|
71432
69912
|
this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
|
|
71433
69913
|
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
71434
69914
|
tags: { deviceId },
|
|
@@ -71656,15 +70136,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71656
70136
|
async listRecentTracks(input) {
|
|
71657
70137
|
return this.queryFacade.listRecentTracks(input);
|
|
71658
70138
|
}
|
|
71659
|
-
async listGroups(input) {
|
|
71660
|
-
return {
|
|
71661
|
-
groups: [],
|
|
71662
|
-
nextCursor: null
|
|
71663
|
-
};
|
|
71664
|
-
}
|
|
71665
|
-
async getGroup(input) {
|
|
71666
|
-
return null;
|
|
71667
|
-
}
|
|
71668
70139
|
/**
|
|
71669
70140
|
* Every event kind the device can produce: built-ins (motion + audio),
|
|
71670
70141
|
* detection classes actually observed on the device, and sensor kinds
|
|
@@ -72183,8 +70654,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
72183
70654
|
},
|
|
72184
70655
|
liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
|
|
72185
70656
|
liveEventIds: (ids) => eventStore.existingEventIds(ids),
|
|
72186
|
-
liveGroupIds: (ids) => this.groupStore?.existingGroupIds(ids) ?? Promise.resolve(/* @__PURE__ */ new Set()),
|
|
72187
|
-
liveGroupsWithMembers: (ids) => this.groupStore?.groupsWithMembers(ids) ?? Promise.resolve(/* @__PURE__ */ new Set()),
|
|
72188
70657
|
reclaim: async (collection, key) => {
|
|
72189
70658
|
if (collection === "pipeline-analytics:media") {
|
|
72190
70659
|
await mediaStore.deleteByKey(key);
|
|
@@ -72947,41 +71416,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
72947
71416
|
buildFaceRescoreActionHandlers() {
|
|
72948
71417
|
return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
|
|
72949
71418
|
}
|
|
72950
|
-
groupQueryPorts() {
|
|
72951
|
-
const groupStore = this.groupStore;
|
|
72952
|
-
if (groupStore === null) return null;
|
|
72953
|
-
return {
|
|
72954
|
-
groupStore,
|
|
72955
|
-
loadTracks: async (trackIds) => {
|
|
72956
|
-
const map = /* @__PURE__ */ new Map();
|
|
72957
|
-
const tracks = this.trackStore;
|
|
72958
|
-
if (!tracks || trackIds.length === 0) return map;
|
|
72959
|
-
const missing = [];
|
|
72960
|
-
for (const id of trackIds) {
|
|
72961
|
-
const active = tracks.getActiveByTrack(id);
|
|
72962
|
-
if (active !== null) map.set(id, trackToLite(active));
|
|
72963
|
-
else missing.push(id);
|
|
72964
|
-
}
|
|
72965
|
-
if (missing.length === 0) return map;
|
|
72966
|
-
for (const persisted of await tracks.getPersistedByTrackIds(missing)) map.set(persisted.trackId, trackToLite(persisted));
|
|
72967
|
-
return map;
|
|
72968
|
-
},
|
|
72969
|
-
eventMediaBaseUrl: () => this.eventMediaBaseUrl
|
|
72970
|
-
};
|
|
72971
|
-
}
|
|
72972
|
-
buildGroupActionHandlers() {
|
|
72973
|
-
return {
|
|
72974
|
-
"analytics.listGroups": async (input) => {
|
|
72975
|
-
return {
|
|
72976
|
-
groups: [],
|
|
72977
|
-
hasMore: false
|
|
72978
|
-
};
|
|
72979
|
-
},
|
|
72980
|
-
"analytics.getGroup": async (input) => {
|
|
72981
|
-
return { group: null };
|
|
72982
|
-
}
|
|
72983
|
-
};
|
|
72984
|
-
}
|
|
72985
71419
|
buildPhotoEnrollActionHandlers() {
|
|
72986
71420
|
return {
|
|
72987
71421
|
"photo.analyze": async (input) => {
|
|
@@ -73367,8 +71801,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73367
71801
|
mediaStore,
|
|
73368
71802
|
faceStore: this.faceStore,
|
|
73369
71803
|
plateStore: this.plateStore,
|
|
73370
|
-
objectEmbeddingStore: this.objectEmbeddingStore
|
|
73371
|
-
groupStore: this.groupStore
|
|
71804
|
+
objectEmbeddingStore: this.objectEmbeddingStore
|
|
73372
71805
|
};
|
|
73373
71806
|
const absent = absentCascadeLegs(stores);
|
|
73374
71807
|
if (absent.length > 0) this.ctx.logger.warn("track cascade is missing a leg — its rows will be left behind", { meta: {
|
|
@@ -73577,8 +72010,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73577
72010
|
media: 0,
|
|
73578
72011
|
faces: 0,
|
|
73579
72012
|
plates: 0,
|
|
73580
|
-
embeddings: 0
|
|
73581
|
-
groups: 0
|
|
72013
|
+
embeddings: 0
|
|
73582
72014
|
};
|
|
73583
72015
|
this.lastPruneBytes = 0;
|
|
73584
72016
|
const cascade = this.buildTrackCascadeRegistry();
|
|
@@ -73638,8 +72070,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
73638
72070
|
media: tally.media,
|
|
73639
72071
|
faces: tally.faces,
|
|
73640
72072
|
plates: tally.plates,
|
|
73641
|
-
embeddings: tally.embeddings
|
|
73642
|
-
groups: tally.groups
|
|
72073
|
+
embeddings: tally.embeddings
|
|
73643
72074
|
};
|
|
73644
72075
|
}
|
|
73645
72076
|
/**
|