@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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-WrUmjaGQ.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -3082,8 +3082,8 @@ async function raceTimeout(call, timeoutMs) {
|
|
|
3082
3082
|
* detail the encoder can see.
|
|
3083
3083
|
*/
|
|
3084
3084
|
async function downscaleJpeg(bytes, maxPx) {
|
|
3085
|
-
const { default: sharp$
|
|
3086
|
-
const out = await sharp$
|
|
3085
|
+
const { default: sharp$15 } = await import("sharp");
|
|
3086
|
+
const out = await sharp$15(Buffer.from(bytes)).resize({
|
|
3087
3087
|
width: maxPx,
|
|
3088
3088
|
height: maxPx,
|
|
3089
3089
|
fit: "inside",
|
|
@@ -9982,8 +9982,8 @@ var NcDispatcher = class {
|
|
|
9982
9982
|
try {
|
|
9983
9983
|
const points = (await this.deps.getZonePolygons?.(deviceId, zoneIds) ?? []).flat();
|
|
9984
9984
|
if (points.length === 0) return null;
|
|
9985
|
-
const { default: sharp$
|
|
9986
|
-
const img = sharp$
|
|
9985
|
+
const { default: sharp$14 } = await import("sharp");
|
|
9986
|
+
const img = sharp$14(Buffer.from(jpeg));
|
|
9987
9987
|
const meta = await img.metadata();
|
|
9988
9988
|
const W = meta.width ?? 0;
|
|
9989
9989
|
const H = meta.height ?? 0;
|
|
@@ -15697,10 +15697,9 @@ function remainingSummaryCandidates(kept, taken, rank) {
|
|
|
15697
15697
|
/**
|
|
15698
15698
|
* The media a tile is drawn from, best first.
|
|
15699
15699
|
*
|
|
15700
|
-
* A digest tile is a SUBJECT, so the crops come first: the
|
|
15701
|
-
* crop (`
|
|
15702
|
-
*
|
|
15703
|
-
* then the clean whole scene (`keyFrame` → `keyFrameSmall` → `fullFrame`).
|
|
15700
|
+
* A digest tile is a SUBJECT, so the crops come first: the TRACK's own subject
|
|
15701
|
+
* crop (`thumbnail`, then its `thumbnailSmall` downscale), and only then the
|
|
15702
|
+
* clean whole scene (`keyFrame` → `keyFrameSmall` → `fullFrame`).
|
|
15704
15703
|
*
|
|
15705
15704
|
* ⚠ The whole-scene rungs are a FALLBACK and stay on the ladder. Measured on
|
|
15706
15705
|
* 60 real tracks (615/617, 24 h): 52 carry a crop, 43 carry a scene rung, and
|
|
@@ -15708,19 +15707,19 @@ function remainingSummaryCandidates(kept, taken, rank) {
|
|
|
15708
15707
|
* two tiles for nothing. A track with neither is dropped, never drawn as an
|
|
15709
15708
|
* empty cell (see `NcSummaryProducer.loadTiles`).
|
|
15710
15709
|
*
|
|
15711
|
-
* ## Why `thumbnail` is
|
|
15710
|
+
* ## Why `thumbnail` is on the ladder (2026-08-27)
|
|
15712
15711
|
*
|
|
15713
15712
|
* It was removed on 2026-08-24 (160200681) because a successful plate read
|
|
15714
15713
|
* overwrites the vehicle first-level crop under the same `thumbnail` kind.
|
|
15715
|
-
* **That collision
|
|
15716
|
-
* `ownerKind: 'group', kind: 'thumbnail'
|
|
15717
|
-
*
|
|
15718
|
-
*
|
|
15719
|
-
*
|
|
15720
|
-
*
|
|
15721
|
-
*
|
|
15722
|
-
* `scripts/check-summary-ladder-reachable.ts` is the guard that
|
|
15723
|
-
*
|
|
15714
|
+
* **That collision belonged to the GROUP entity** — its sealer was what wrote
|
|
15715
|
+
* `ownerKind: 'group', kind: 'thumbnail'`. On a TRACK, plates live under
|
|
15716
|
+
* `ownerKind: 'plate'` / `kind: 'plateCrop'` and never touch `thumbnail`. The
|
|
15717
|
+
* same commit turned the group process off, so the two group-only head rungs
|
|
15718
|
+
* became unreachable and every digest tile fell through to `keyFrame` — a
|
|
15719
|
+
* whole frame — for three days. The group entity was deleted outright on
|
|
15720
|
+
* 2026-09-01 and its rungs went with it;
|
|
15721
|
+
* `scripts/check-summary-ladder-reachable.ts` is the guard that keeps the
|
|
15722
|
+
* best reachable rung a subject crop.
|
|
15724
15723
|
*
|
|
15725
15724
|
* Boxed frames stay absent: a drawn rectangle is a permanent lie in a digest
|
|
15726
15725
|
* tile. Plate and face crops stay absent: a digest tile of a targa is the
|
|
@@ -15728,8 +15727,6 @@ function remainingSummaryCandidates(kept, taken, rank) {
|
|
|
15728
15727
|
* `frameDedupeKey`, not by hiding the whole-scene fallback.
|
|
15729
15728
|
*/
|
|
15730
15729
|
var SUMMARY_MEDIA_LADDER = [
|
|
15731
|
-
"groupCrop",
|
|
15732
|
-
"nativeFrame",
|
|
15733
15730
|
"thumbnail",
|
|
15734
15731
|
"thumbnailSmall",
|
|
15735
15732
|
"keyFrame",
|
|
@@ -15764,7 +15761,8 @@ var SUMMARY_SIZE_GATED_RUNGS = new Set(["thumbnail"]);
|
|
|
15764
15761
|
* Is this row a picture, or a template wearing a picture's kind?
|
|
15765
15762
|
*
|
|
15766
15763
|
* Only a size the index REPORTED can demote a row. A `null` size is unknown,
|
|
15767
|
-
* and demoting on unknown would silently delete every
|
|
15764
|
+
* and demoting on unknown would silently delete every row an index could not
|
|
15765
|
+
* measure.
|
|
15768
15766
|
*/
|
|
15769
15767
|
function rowClearsSizeFloor(row, rung) {
|
|
15770
15768
|
if (!SUMMARY_SIZE_GATED_RUNGS.has(rung)) return true;
|
|
@@ -17395,8 +17393,8 @@ async function deriveSyntheticMedia(jpeg, bbox, timestamp) {
|
|
|
17395
17393
|
];
|
|
17396
17394
|
if (bbox === void 0) return files;
|
|
17397
17395
|
try {
|
|
17398
|
-
const { default: sharp$
|
|
17399
|
-
const meta = await sharp$
|
|
17396
|
+
const { default: sharp$13 } = await import("sharp");
|
|
17397
|
+
const meta = await sharp$13(Buffer.from(jpeg)).metadata();
|
|
17400
17398
|
const W = meta.width ?? 0;
|
|
17401
17399
|
const H = meta.height ?? 0;
|
|
17402
17400
|
if (W === 0 || H === 0) return files;
|
|
@@ -17404,7 +17402,7 @@ async function deriveSyntheticMedia(jpeg, bbox, timestamp) {
|
|
|
17404
17402
|
const top = Math.max(0, Math.min(H - 1, Math.floor(bbox.y * H)));
|
|
17405
17403
|
const width = Math.max(1, Math.min(W - left, Math.ceil(bbox.w * W)));
|
|
17406
17404
|
const height = Math.max(1, Math.min(H - top, Math.ceil(bbox.h * H)));
|
|
17407
|
-
const cropBuf = await sharp$
|
|
17405
|
+
const cropBuf = await sharp$13(Buffer.from(jpeg)).extract({
|
|
17408
17406
|
left,
|
|
17409
17407
|
top,
|
|
17410
17408
|
width,
|
|
@@ -17426,7 +17424,7 @@ async function deriveSyntheticMedia(jpeg, bbox, timestamp) {
|
|
|
17426
17424
|
});
|
|
17427
17425
|
const stroke = Math.max(2, Math.round(Math.min(W, H) / 240));
|
|
17428
17426
|
const overlay = Buffer.from(`<svg width="${W}" height="${H}"><rect x="${left}" y="${top}" width="${width}" height="${height}" fill="none" stroke="#22d3ee" stroke-width="${stroke}"/></svg>`);
|
|
17429
|
-
const boxedBuf = await sharp$
|
|
17427
|
+
const boxedBuf = await sharp$13(Buffer.from(jpeg)).composite([{
|
|
17430
17428
|
input: overlay,
|
|
17431
17429
|
top: 0,
|
|
17432
17430
|
left: 0
|
|
@@ -25958,10 +25956,29 @@ var OWNER_KINDS = [
|
|
|
25958
25956
|
"identity",
|
|
25959
25957
|
"plate",
|
|
25960
25958
|
"vehicle",
|
|
25961
|
-
"scene"
|
|
25962
|
-
"group"
|
|
25959
|
+
"scene"
|
|
25963
25960
|
];
|
|
25964
25961
|
/**
|
|
25962
|
+
* Owner kinds that NOTHING may write any more, but whose rows can still be on
|
|
25963
|
+
* disk from before the owning code was deleted.
|
|
25964
|
+
*
|
|
25965
|
+
* `'group'` is the co-moving group entity, removed on 2026-09-01 together with
|
|
25966
|
+
* `pipeline-analytics:track-groups` / `:track-group-members`
|
|
25967
|
+
* ([D183](../../../../../docs/decisions/adr-0183-code-that-is-deleted-takes-its-tables-with-it.md)).
|
|
25968
|
+
* The tables are dropped at boot from `RETIRED_COLLECTIONS`, so a group-owned
|
|
25969
|
+
* media row can never be claimed again: `resolveMediaOwner` maps it to
|
|
25970
|
+
* `{ kind: 'retired-owner' }` and the orphan audit reclaims it under the same
|
|
25971
|
+
* grace window and ceilings as any other orphan.
|
|
25972
|
+
*
|
|
25973
|
+
* Deliberately NOT a member of {@link OWNER_KINDS}: the type is what stops a
|
|
25974
|
+
* new writer, and this list is only what the READ side must still recognise.
|
|
25975
|
+
*/
|
|
25976
|
+
var RETIRED_OWNER_KINDS = ["group"];
|
|
25977
|
+
/** Is this an owner kind whose owning code is gone? See {@link RETIRED_OWNER_KINDS}. */
|
|
25978
|
+
function isRetiredMediaOwnerKind(value) {
|
|
25979
|
+
return RETIRED_OWNER_KINDS.includes(value);
|
|
25980
|
+
}
|
|
25981
|
+
/**
|
|
25965
25982
|
* Narrow an arbitrary string to an `OwnerKind`.
|
|
25966
25983
|
*
|
|
25967
25984
|
* Exists so a caller that builds owner scopes from data (the training-export
|
|
@@ -27091,10 +27108,6 @@ var MediaStore = class {
|
|
|
27091
27108
|
async deleteForTracks(trackIds) {
|
|
27092
27109
|
return this.deleteForOwner("track", trackIds);
|
|
27093
27110
|
}
|
|
27094
|
-
/** Delete the group's OWN crop. The native frame is a pointer and is not here. */
|
|
27095
|
-
async deleteByGroups(groupIds) {
|
|
27096
|
-
return this.deleteForOwnerCounted("group", groupIds);
|
|
27097
|
-
}
|
|
27098
27111
|
/**
|
|
27099
27112
|
* `TrackScopedStore` contract for the retention cascade: delete all media
|
|
27100
27113
|
* owned by the given tracks — the tracks' OWN media (`ownerKind:'track'`) plus
|
|
@@ -33963,7 +33976,6 @@ var EMPTY_CASCADE_TALLY = {
|
|
|
33963
33976
|
faces: 0,
|
|
33964
33977
|
plates: 0,
|
|
33965
33978
|
embeddings: 0,
|
|
33966
|
-
groups: 0,
|
|
33967
33979
|
bytesReclaimed: 0
|
|
33968
33980
|
};
|
|
33969
33981
|
/** Fold a leaf store's receipts into a running tally. */
|
|
@@ -34007,13 +34019,6 @@ function addLeafCounts(tally, counts) {
|
|
|
34007
34019
|
bytesReclaimed
|
|
34008
34020
|
};
|
|
34009
34021
|
break;
|
|
34010
|
-
case "groups":
|
|
34011
|
-
next = {
|
|
34012
|
-
...next,
|
|
34013
|
-
groups: next.groups + c.rows,
|
|
34014
|
-
bytesReclaimed
|
|
34015
|
-
};
|
|
34016
|
-
break;
|
|
34017
34022
|
}
|
|
34018
34023
|
}
|
|
34019
34024
|
return next;
|
|
@@ -34027,7 +34032,6 @@ function mergeCascadeTallies(a, b) {
|
|
|
34027
34032
|
faces: a.faces + b.faces,
|
|
34028
34033
|
plates: a.plates + b.plates,
|
|
34029
34034
|
embeddings: a.embeddings + b.embeddings,
|
|
34030
|
-
groups: a.groups + b.groups,
|
|
34031
34035
|
bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
|
|
34032
34036
|
};
|
|
34033
34037
|
}
|
|
@@ -34090,7 +34094,6 @@ function absentCascadeLegs(stores) {
|
|
|
34090
34094
|
if (!stores.faceStore) absent.push("faces");
|
|
34091
34095
|
if (!stores.plateStore) absent.push("plates");
|
|
34092
34096
|
if (!stores.objectEmbeddingStore) absent.push("embeddings");
|
|
34093
|
-
if (!stores.groupStore) absent.push("groups");
|
|
34094
34097
|
return absent;
|
|
34095
34098
|
}
|
|
34096
34099
|
function buildAnalyticsCascadeRegistry(stores) {
|
|
@@ -34098,7 +34101,6 @@ function buildAnalyticsCascadeRegistry(stores) {
|
|
|
34098
34101
|
if (stores.faceStore) registry.push(stores.faceStore);
|
|
34099
34102
|
if (stores.plateStore) registry.push(stores.plateStore);
|
|
34100
34103
|
if (stores.objectEmbeddingStore) registry.push(stores.objectEmbeddingStore);
|
|
34101
|
-
if (stores.groupStore) registry.push(stores.groupStore);
|
|
34102
34104
|
return registry;
|
|
34103
34105
|
}
|
|
34104
34106
|
/**
|
|
@@ -47843,1420 +47845,6 @@ function shouldAutoReembed(samples, targetModelId, knownPixelless, passRunning)
|
|
|
47843
47845
|
if (passRunning || targetModelId === null || targetModelId === "") return false;
|
|
47844
47846
|
return samples.some((s) => s.modelId !== targetModelId && s.mediaKey !== void 0 && !knownPixelless.has(s.sampleId));
|
|
47845
47847
|
}
|
|
47846
|
-
var ListGroupsInputSchema = require_dist.object({
|
|
47847
|
-
deviceId: require_dist.number().int().optional(),
|
|
47848
|
-
sinceMs: require_dist.number().int().optional(),
|
|
47849
|
-
untilMs: require_dist.number().int().optional(),
|
|
47850
|
-
limit: require_dist.number().int().min(1).max(100).optional(),
|
|
47851
|
-
offset: require_dist.number().int().min(0).optional()
|
|
47852
|
-
});
|
|
47853
|
-
var groupActions = require_dist.defineCustomActions({
|
|
47854
|
-
"analytics.listGroups": require_dist.customAction(ListGroupsInputSchema, require_dist.object({
|
|
47855
|
-
groups: require_dist.array(require_dist.AnalyticsGroupRecordSchema).readonly(),
|
|
47856
|
-
hasMore: require_dist.boolean()
|
|
47857
|
-
})),
|
|
47858
|
-
"analytics.getGroup": require_dist.customAction(require_dist.object({ groupId: require_dist.string().min(1) }), require_dist.object({ group: require_dist.AnalyticsGroupDetailSchema.nullable() }))
|
|
47859
|
-
});
|
|
47860
|
-
var MATCH_WINDOW_MS = 1e3;
|
|
47861
|
-
var MOVE_EPS = .35;
|
|
47862
|
-
/** Live-assigner window: same predicate, recent positions only. */
|
|
47863
|
-
var LIVE_WINDOW_MS = 3e3;
|
|
47864
|
-
/** Registry stationary is out; audio has no bbox and is never a member or a singleton. */
|
|
47865
|
-
function isEligibleMember(track) {
|
|
47866
|
-
if (track.closedReason === "stationary-promotion") return false;
|
|
47867
|
-
if (track.className === "audio") return false;
|
|
47868
|
-
return true;
|
|
47869
|
-
}
|
|
47870
|
-
function diag(bbox) {
|
|
47871
|
-
return Math.hypot(bbox.w, bbox.h);
|
|
47872
|
-
}
|
|
47873
|
-
function nearestPosition(positions, t) {
|
|
47874
|
-
let best;
|
|
47875
|
-
let bestDt = 1001;
|
|
47876
|
-
for (const p of positions) {
|
|
47877
|
-
const dt = Math.abs(p.timestamp - t);
|
|
47878
|
-
if (dt <= 1e3 && dt < bestDt) {
|
|
47879
|
-
best = p;
|
|
47880
|
-
bestDt = dt;
|
|
47881
|
-
}
|
|
47882
|
-
}
|
|
47883
|
-
return best;
|
|
47884
|
-
}
|
|
47885
|
-
function meanDiag(positions) {
|
|
47886
|
-
if (positions.length === 0) return 0;
|
|
47887
|
-
let sum = 0;
|
|
47888
|
-
for (const p of positions) sum += diag(p.bbox);
|
|
47889
|
-
return sum / positions.length;
|
|
47890
|
-
}
|
|
47891
|
-
function vecLen(v) {
|
|
47892
|
-
return Math.hypot(v.x, v.y);
|
|
47893
|
-
}
|
|
47894
|
-
function cosine(a, b) {
|
|
47895
|
-
const la = vecLen(a);
|
|
47896
|
-
const lb = vecLen(b);
|
|
47897
|
-
if (la === 0 || lb === 0) return 1;
|
|
47898
|
-
return (a.x * b.x + a.y * b.y) / (la * lb);
|
|
47899
|
-
}
|
|
47900
|
-
function positionsInWindow(track, start, end) {
|
|
47901
|
-
return track.positions.filter((p) => p.timestamp >= start && p.timestamp <= end);
|
|
47902
|
-
}
|
|
47903
|
-
function windowDisplacement(positions) {
|
|
47904
|
-
const first = positions[0];
|
|
47905
|
-
const last = positions[positions.length - 1];
|
|
47906
|
-
if (!first || !last) return {
|
|
47907
|
-
x: 0,
|
|
47908
|
-
y: 0
|
|
47909
|
-
};
|
|
47910
|
-
const a = bboxCentroid(first.bbox);
|
|
47911
|
-
const b = bboxCentroid(last.bbox);
|
|
47912
|
-
return {
|
|
47913
|
-
x: b.x - a.x,
|
|
47914
|
-
y: b.y - a.y
|
|
47915
|
-
};
|
|
47916
|
-
}
|
|
47917
|
-
function windowRelativeMotion(positions) {
|
|
47918
|
-
const mean = meanDiag(positions);
|
|
47919
|
-
if (mean <= 0) return 0;
|
|
47920
|
-
return vecLen(windowDisplacement(positions)) / mean;
|
|
47921
|
-
}
|
|
47922
|
-
/**
|
|
47923
|
-
* Same cohesion + crossing veto as {@link pairTogether}, over `[now − 3 s, now]`.
|
|
47924
|
-
* Displacement/cosine use the window, not the whole track.
|
|
47925
|
-
*/
|
|
47926
|
-
function pairTogetherNow(a, b, now) {
|
|
47927
|
-
const start = now - LIVE_WINDOW_MS;
|
|
47928
|
-
const overlapStart = Math.max(a.firstSeen, b.firstSeen, start);
|
|
47929
|
-
const overlapEnd = Math.min(a.lastSeen, b.lastSeen, now);
|
|
47930
|
-
if (overlapEnd < overlapStart) return false;
|
|
47931
|
-
let joint = 0;
|
|
47932
|
-
let close = 0;
|
|
47933
|
-
for (let t = overlapStart; t <= overlapEnd; t += 500) {
|
|
47934
|
-
const pa = nearestPosition(a.positions, t);
|
|
47935
|
-
const pb = nearestPosition(b.positions, t);
|
|
47936
|
-
if (!pa || !pb) continue;
|
|
47937
|
-
joint++;
|
|
47938
|
-
const ca = bboxCentroid(pa.bbox);
|
|
47939
|
-
const cb = bboxCentroid(pb.bbox);
|
|
47940
|
-
const gap = Math.hypot(ca.x - cb.x, ca.y - cb.y);
|
|
47941
|
-
const scale = (diag(pa.bbox) + diag(pb.bbox)) / 2;
|
|
47942
|
-
if ((scale > 0 ? gap / scale : Number.POSITIVE_INFINITY) <= 1.5) close++;
|
|
47943
|
-
}
|
|
47944
|
-
if (joint === 0) return false;
|
|
47945
|
-
if (close / joint < .5) return false;
|
|
47946
|
-
const wa = positionsInWindow(a, start, now);
|
|
47947
|
-
const wb = positionsInWindow(b, start, now);
|
|
47948
|
-
const aMoves = windowRelativeMotion(wa) >= MOVE_EPS;
|
|
47949
|
-
const bMoves = windowRelativeMotion(wb) >= MOVE_EPS;
|
|
47950
|
-
if (aMoves && bMoves && cosine(windowDisplacement(wa), windowDisplacement(wb)) < .3) return false;
|
|
47951
|
-
return true;
|
|
47952
|
-
}
|
|
47953
|
-
var GroupLiveAssigner = class {
|
|
47954
|
-
nextId;
|
|
47955
|
-
snapshots = /* @__PURE__ */ new Map();
|
|
47956
|
-
groups = /* @__PURE__ */ new Map();
|
|
47957
|
-
membership = /* @__PURE__ */ new Map();
|
|
47958
|
-
/** `${trackId}>${groupId}` → first instant the join predicate held. */
|
|
47959
|
-
joinSince = /* @__PURE__ */ new Map();
|
|
47960
|
-
/** trackId → first instant they failed pair against every remaining member. */
|
|
47961
|
-
leaveSince = /* @__PURE__ */ new Map();
|
|
47962
|
-
constructor(nextId = () => (0, node_crypto.randomUUID)()) {
|
|
47963
|
-
this.nextId = nextId;
|
|
47964
|
-
}
|
|
47965
|
-
observe(track, now) {
|
|
47966
|
-
this.snapshots.set(track.trackId, track);
|
|
47967
|
-
if (!isEligibleMember(track)) {
|
|
47968
|
-
this.removeFromGroup(track.trackId);
|
|
47969
|
-
return;
|
|
47970
|
-
}
|
|
47971
|
-
if (!this.membership.has(track.trackId)) this.birthSingleton(track, now);
|
|
47972
|
-
this.considerLeave(track, now);
|
|
47973
|
-
const groupId = this.membership.get(track.trackId);
|
|
47974
|
-
const group = groupId !== void 0 ? this.groups.get(groupId) : void 0;
|
|
47975
|
-
if (group) for (const id of [...group.members]) {
|
|
47976
|
-
if (id === track.trackId) continue;
|
|
47977
|
-
const snap = this.snapshots.get(id);
|
|
47978
|
-
if (snap) this.considerLeave(snap, now);
|
|
47979
|
-
}
|
|
47980
|
-
this.considerJoin(track, now);
|
|
47981
|
-
}
|
|
47982
|
-
groupOf(trackId) {
|
|
47983
|
-
return this.membership.get(trackId);
|
|
47984
|
-
}
|
|
47985
|
-
snapshot(trackId) {
|
|
47986
|
-
return this.snapshots.get(trackId);
|
|
47987
|
-
}
|
|
47988
|
-
liveGroups() {
|
|
47989
|
-
return [...this.groups.values()].map((g) => this.toLive(g));
|
|
47990
|
-
}
|
|
47991
|
-
idleGroups(now, idleMs) {
|
|
47992
|
-
return this.liveGroups().filter((g) => now >= g.lastActiveAt + idleMs);
|
|
47993
|
-
}
|
|
47994
|
-
drop(ids) {
|
|
47995
|
-
for (const id of ids) {
|
|
47996
|
-
const g = this.groups.get(id);
|
|
47997
|
-
if (!g) continue;
|
|
47998
|
-
for (const member of g.members) this.membership.delete(member);
|
|
47999
|
-
this.groups.delete(id);
|
|
48000
|
-
}
|
|
48001
|
-
}
|
|
48002
|
-
birthSingleton(track, now) {
|
|
48003
|
-
const id = this.nextId();
|
|
48004
|
-
this.groups.set(id, {
|
|
48005
|
-
id,
|
|
48006
|
-
deviceId: track.deviceId,
|
|
48007
|
-
openedAt: now,
|
|
48008
|
-
members: new Set([track.trackId])
|
|
48009
|
-
});
|
|
48010
|
-
this.membership.set(track.trackId, id);
|
|
48011
|
-
}
|
|
48012
|
-
considerLeave(track, now) {
|
|
48013
|
-
const groupId = this.membership.get(track.trackId);
|
|
48014
|
-
if (groupId === void 0) return;
|
|
48015
|
-
const group = this.groups.get(groupId);
|
|
48016
|
-
if (!group || group.members.size < 2) {
|
|
48017
|
-
this.leaveSince.delete(track.trackId);
|
|
48018
|
-
return;
|
|
48019
|
-
}
|
|
48020
|
-
if ([...group.members].filter((id) => id !== track.trackId).some((id) => {
|
|
48021
|
-
const other = this.snapshots.get(id);
|
|
48022
|
-
return other !== void 0 && pairTogetherNow(track, other, now);
|
|
48023
|
-
})) {
|
|
48024
|
-
this.leaveSince.delete(track.trackId);
|
|
48025
|
-
return;
|
|
48026
|
-
}
|
|
48027
|
-
const since = this.leaveSince.get(track.trackId) ?? now;
|
|
48028
|
-
this.leaveSince.set(track.trackId, since);
|
|
48029
|
-
if (now - since < 3e3) return;
|
|
48030
|
-
this.peel(track, now);
|
|
48031
|
-
}
|
|
48032
|
-
peel(track, now) {
|
|
48033
|
-
this.removeFromGroup(track.trackId);
|
|
48034
|
-
this.leaveSince.delete(track.trackId);
|
|
48035
|
-
this.birthSingleton(track, now);
|
|
48036
|
-
}
|
|
48037
|
-
considerJoin(track, now) {
|
|
48038
|
-
const mineId = this.membership.get(track.trackId);
|
|
48039
|
-
if (mineId === void 0) return;
|
|
48040
|
-
const mine = this.groups.get(mineId);
|
|
48041
|
-
if (!mine || mine.members.size !== 1) {
|
|
48042
|
-
this.clearJoinClocks(track.trackId);
|
|
48043
|
-
return;
|
|
48044
|
-
}
|
|
48045
|
-
let best;
|
|
48046
|
-
for (const group of this.groups.values()) {
|
|
48047
|
-
if (group.id === mineId || group.deviceId !== track.deviceId) continue;
|
|
48048
|
-
let gap = Number.POSITIVE_INFINITY;
|
|
48049
|
-
let together = false;
|
|
48050
|
-
for (const memberId of group.members) {
|
|
48051
|
-
const other = this.snapshots.get(memberId);
|
|
48052
|
-
if (!other) continue;
|
|
48053
|
-
if (pairTogetherNow(track, other, now)) {
|
|
48054
|
-
together = true;
|
|
48055
|
-
gap = Math.min(gap, centroidGap(track, other, now));
|
|
48056
|
-
}
|
|
48057
|
-
}
|
|
48058
|
-
if (!together) continue;
|
|
48059
|
-
if (best === void 0 || gap < best.gap) best = {
|
|
48060
|
-
groupId: group.id,
|
|
48061
|
-
gap
|
|
48062
|
-
};
|
|
48063
|
-
}
|
|
48064
|
-
if (best === void 0) {
|
|
48065
|
-
this.clearJoinClocks(track.trackId);
|
|
48066
|
-
return;
|
|
48067
|
-
}
|
|
48068
|
-
const key = `${track.trackId}>${best.groupId}`;
|
|
48069
|
-
for (const existing of [...this.joinSince.keys()]) if (existing.startsWith(`${track.trackId}>`) && existing !== key) this.joinSince.delete(existing);
|
|
48070
|
-
const since = this.joinSince.get(key) ?? now;
|
|
48071
|
-
this.joinSince.set(key, since);
|
|
48072
|
-
if (now - since < 1500) return;
|
|
48073
|
-
this.absorb(track.trackId, mineId, best.groupId);
|
|
48074
|
-
}
|
|
48075
|
-
absorb(trackId, fromId, intoId) {
|
|
48076
|
-
const into = this.groups.get(intoId);
|
|
48077
|
-
if (!into) return;
|
|
48078
|
-
into.members.add(trackId);
|
|
48079
|
-
this.membership.set(trackId, intoId);
|
|
48080
|
-
this.groups.delete(fromId);
|
|
48081
|
-
this.clearJoinClocks(trackId);
|
|
48082
|
-
this.leaveSince.delete(trackId);
|
|
48083
|
-
}
|
|
48084
|
-
removeFromGroup(trackId) {
|
|
48085
|
-
const groupId = this.membership.get(trackId);
|
|
48086
|
-
this.membership.delete(trackId);
|
|
48087
|
-
this.clearJoinClocks(trackId);
|
|
48088
|
-
this.leaveSince.delete(trackId);
|
|
48089
|
-
if (groupId === void 0) return;
|
|
48090
|
-
const group = this.groups.get(groupId);
|
|
48091
|
-
if (!group) return;
|
|
48092
|
-
group.members.delete(trackId);
|
|
48093
|
-
if (group.members.size === 0) this.groups.delete(groupId);
|
|
48094
|
-
}
|
|
48095
|
-
clearJoinClocks(trackId) {
|
|
48096
|
-
for (const key of [...this.joinSince.keys()]) if (key.startsWith(`${trackId}>`)) this.joinSince.delete(key);
|
|
48097
|
-
}
|
|
48098
|
-
toLive(group) {
|
|
48099
|
-
let lastActiveAt = group.openedAt;
|
|
48100
|
-
for (const id of group.members) {
|
|
48101
|
-
const snap = this.snapshots.get(id);
|
|
48102
|
-
if (snap && snap.lastSeen > lastActiveAt) lastActiveAt = snap.lastSeen;
|
|
48103
|
-
}
|
|
48104
|
-
return {
|
|
48105
|
-
id: group.id,
|
|
48106
|
-
deviceId: group.deviceId,
|
|
48107
|
-
memberTrackIds: [...group.members],
|
|
48108
|
-
openedAt: group.openedAt,
|
|
48109
|
-
lastActiveAt
|
|
48110
|
-
};
|
|
48111
|
-
}
|
|
48112
|
-
};
|
|
48113
|
-
function centroidGap(a, b, now) {
|
|
48114
|
-
const pa = a.positions.reduce((best, p) => Math.abs(p.timestamp - now) < Math.abs(best.timestamp - now) ? p : best, a.positions[0]);
|
|
48115
|
-
const pb = b.positions.reduce((best, p) => Math.abs(p.timestamp - now) < Math.abs(best.timestamp - now) ? p : best, b.positions[0]);
|
|
48116
|
-
if (!pa || !pb) return Number.POSITIVE_INFINITY;
|
|
48117
|
-
const ca = bboxCentroid(pa.bbox);
|
|
48118
|
-
const cb = bboxCentroid(pb.bbox);
|
|
48119
|
-
return Math.hypot(ca.x - cb.x, ca.y - cb.y);
|
|
48120
|
-
}
|
|
48121
|
-
//#endregion
|
|
48122
|
-
//#region src/pipeline-analytics/group/group-session.ts
|
|
48123
|
-
/** Same idle as the notification burst (`NC_GROUP_DEFAULT_IDLE_SEC`). */
|
|
48124
|
-
var GROUP_SESSION_IDLE_MS = 3e4;
|
|
48125
|
-
//#endregion
|
|
48126
|
-
//#region src/pipeline-analytics/group/group-live-buffer.ts
|
|
48127
|
-
function toGroupableTrack(track) {
|
|
48128
|
-
return {
|
|
48129
|
-
trackId: track.trackId,
|
|
48130
|
-
deviceId: track.deviceId,
|
|
48131
|
-
className: track.className,
|
|
48132
|
-
firstSeen: track.firstSeen,
|
|
48133
|
-
lastSeen: track.lastSeen,
|
|
48134
|
-
positions: track.positions.map((p) => ({
|
|
48135
|
-
...p,
|
|
48136
|
-
bbox: { ...p.bbox }
|
|
48137
|
-
})),
|
|
48138
|
-
...track.closedReason !== void 0 ? { closedReason: track.closedReason } : {}
|
|
48139
|
-
};
|
|
48140
|
-
}
|
|
48141
|
-
var GroupLiveBuffer = class {
|
|
48142
|
-
assigner;
|
|
48143
|
-
primed = /* @__PURE__ */ new Set();
|
|
48144
|
-
constructor(nextId) {
|
|
48145
|
-
this.assigner = new GroupLiveAssigner(nextId);
|
|
48146
|
-
}
|
|
48147
|
-
observe(track, now) {
|
|
48148
|
-
this.assigner.observe(track, now);
|
|
48149
|
-
}
|
|
48150
|
-
groupOf(trackId) {
|
|
48151
|
-
return this.assigner.groupOf(trackId);
|
|
48152
|
-
}
|
|
48153
|
-
snapshot(trackId) {
|
|
48154
|
-
return this.assigner.snapshot(trackId);
|
|
48155
|
-
}
|
|
48156
|
-
liveGroups() {
|
|
48157
|
-
return this.assigner.liveGroups();
|
|
48158
|
-
}
|
|
48159
|
-
idleGroups(now, idleMs = GROUP_SESSION_IDLE_MS) {
|
|
48160
|
-
return this.assigner.idleGroups(now, idleMs);
|
|
48161
|
-
}
|
|
48162
|
-
drop(ids) {
|
|
48163
|
-
this.assigner.drop(ids);
|
|
48164
|
-
}
|
|
48165
|
-
isPrimed(deviceId) {
|
|
48166
|
-
return this.primed.has(deviceId);
|
|
48167
|
-
}
|
|
48168
|
-
markPrimed(deviceId) {
|
|
48169
|
-
this.primed.add(deviceId);
|
|
48170
|
-
}
|
|
48171
|
-
};
|
|
48172
|
-
function trackToLite(track) {
|
|
48173
|
-
return {
|
|
48174
|
-
trackId: track.trackId,
|
|
48175
|
-
className: track.className,
|
|
48176
|
-
classes: track.classes,
|
|
48177
|
-
firstSeen: track.firstSeen,
|
|
48178
|
-
lastSeen: track.lastSeen
|
|
48179
|
-
};
|
|
48180
|
-
}
|
|
48181
|
-
/**
|
|
48182
|
-
* Gallery and digest read the same ladder: groupCrop, then the native-frame
|
|
48183
|
-
* pointer, then a member's clean full frame. Plate / face / member
|
|
48184
|
-
* thumbnails never reach the picker — a plate read overwrites the vehicle
|
|
48185
|
-
* first-level crop under the same `thumbnail` kind. A multi with no crop
|
|
48186
|
-
* still has a picture if the sealer stored `nativeKey`.
|
|
48187
|
-
*/
|
|
48188
|
-
var DIGEST_MEMBER_FALLBACK_KINDS = new Set([
|
|
48189
|
-
"keyFrame",
|
|
48190
|
-
"keyFrameSmall",
|
|
48191
|
-
"fullFrame"
|
|
48192
|
-
]);
|
|
48193
|
-
function digestMediaRows(input) {
|
|
48194
|
-
const owned = [];
|
|
48195
|
-
if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
|
|
48196
|
-
key: input.mediaKey,
|
|
48197
|
-
kind: "groupCrop",
|
|
48198
|
-
timestamp: input.timestamp,
|
|
48199
|
-
sizeBytes: null
|
|
48200
|
-
});
|
|
48201
|
-
if (input.nativeKey !== null && input.nativeKey.length > 0) owned.push({
|
|
48202
|
-
key: input.nativeKey,
|
|
48203
|
-
kind: "nativeFrame",
|
|
48204
|
-
timestamp: input.timestamp,
|
|
48205
|
-
sizeBytes: null
|
|
48206
|
-
});
|
|
48207
|
-
if (owned.length > 0) return owned;
|
|
48208
|
-
return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
|
|
48209
|
-
}
|
|
48210
|
-
//#endregion
|
|
48211
|
-
//#region src/pipeline-analytics/group/group-crop.ts
|
|
48212
|
-
/** Always containSubject: true. Never the 0.5 delivered threshold. */
|
|
48213
|
-
function groupWideCentralSquareLayout(unionBbox, frame) {
|
|
48214
|
-
return wideCentralSquareLayout(unionBbox, frame, { containSubject: true });
|
|
48215
|
-
}
|
|
48216
|
-
async function cropGroupFromRgb(rgb, frameWidth, frameHeight, unionBbox) {
|
|
48217
|
-
const layout = groupWideCentralSquareLayout(unionBbox, {
|
|
48218
|
-
W: frameWidth,
|
|
48219
|
-
H: frameHeight
|
|
48220
|
-
});
|
|
48221
|
-
const { crop: slab } = await extractCrop(rgb, frameWidth, frameHeight, layout.fetch);
|
|
48222
|
-
return composeWideCentralSquareThumbnail(slab, layout);
|
|
48223
|
-
}
|
|
48224
|
-
//#endregion
|
|
48225
|
-
//#region src/pipeline-analytics/group/group-instant.ts
|
|
48226
|
-
/**
|
|
48227
|
-
* Chosen instant + union bbox for a group, at close, over stored positions[]
|
|
48228
|
-
* (`docs/design/2026-08-21-group-entity-and-group-media.md` §4.1).
|
|
48229
|
-
*
|
|
48230
|
-
* Tier order — the first that separates two candidates decides:
|
|
48231
|
-
* 1. completeness (members with a position within ±1 s)
|
|
48232
|
-
* 2. square containment of the union (D212)
|
|
48233
|
-
* 3. semantic (a member's confirmed recognition)
|
|
48234
|
-
* 4. edge-clear (union touches no frame border)
|
|
48235
|
-
* 5. effective score = mean(confidence) × containment × sizeFactor
|
|
48236
|
-
*
|
|
48237
|
-
* Retrospective. No new capture path.
|
|
48238
|
-
*/
|
|
48239
|
-
var GROUP_MEDIA_MATCH_MS = 2e3;
|
|
48240
|
-
function nearest(positions, t, windowMs) {
|
|
48241
|
-
let best;
|
|
48242
|
-
let bestDt = windowMs + 1;
|
|
48243
|
-
for (const p of positions) {
|
|
48244
|
-
const dt = Math.abs(p.timestamp - t);
|
|
48245
|
-
if (dt <= windowMs && dt < bestDt) {
|
|
48246
|
-
best = p;
|
|
48247
|
-
bestDt = dt;
|
|
48248
|
-
}
|
|
48249
|
-
}
|
|
48250
|
-
return best;
|
|
48251
|
-
}
|
|
48252
|
-
function unionOf(boxes) {
|
|
48253
|
-
if (boxes.length === 0) return null;
|
|
48254
|
-
let x0 = Infinity;
|
|
48255
|
-
let y0 = Infinity;
|
|
48256
|
-
let x1 = -Infinity;
|
|
48257
|
-
let y1 = -Infinity;
|
|
48258
|
-
for (const b of boxes) {
|
|
48259
|
-
x0 = Math.min(x0, b.x);
|
|
48260
|
-
y0 = Math.min(y0, b.y);
|
|
48261
|
-
x1 = Math.max(x1, b.x + b.w);
|
|
48262
|
-
y1 = Math.max(y1, b.y + b.h);
|
|
48263
|
-
}
|
|
48264
|
-
return {
|
|
48265
|
-
x: x0,
|
|
48266
|
-
y: y0,
|
|
48267
|
-
w: x1 - x0,
|
|
48268
|
-
h: y1 - y0
|
|
48269
|
-
};
|
|
48270
|
-
}
|
|
48271
|
-
function touchesBorder(box, frame, tol = 1) {
|
|
48272
|
-
return box.x <= tol || box.y <= tol || box.x + box.w >= frame.width - tol || box.y + box.h >= frame.height - tol;
|
|
48273
|
-
}
|
|
48274
|
-
function scoreInstant(members, t, frame) {
|
|
48275
|
-
const boxes = [];
|
|
48276
|
-
let semantic = false;
|
|
48277
|
-
let confSum = 0;
|
|
48278
|
-
let confN = 0;
|
|
48279
|
-
for (const m of members) {
|
|
48280
|
-
const p = nearest(m.positions, t, MATCH_WINDOW_MS);
|
|
48281
|
-
if (!p) continue;
|
|
48282
|
-
boxes.push(p.bbox);
|
|
48283
|
-
if (m.semantic === true) semantic = true;
|
|
48284
|
-
if (m.confidence !== void 0) {
|
|
48285
|
-
confSum += m.confidence;
|
|
48286
|
-
confN += 1;
|
|
48287
|
-
}
|
|
48288
|
-
}
|
|
48289
|
-
const union = unionOf(boxes);
|
|
48290
|
-
if (!union) return null;
|
|
48291
|
-
const square = centralSquareContainmentFraction(union, {
|
|
48292
|
-
W: frame.width,
|
|
48293
|
-
H: frame.height
|
|
48294
|
-
});
|
|
48295
|
-
const containment = bboxContainment(union, frame.width, frame.height);
|
|
48296
|
-
const size = bboxSizeScore(union, frame.width, frame.height);
|
|
48297
|
-
const meanConf = confN > 0 ? confSum / confN : 1;
|
|
48298
|
-
return {
|
|
48299
|
-
timestamp: t,
|
|
48300
|
-
union,
|
|
48301
|
-
visibleCount: boxes.length,
|
|
48302
|
-
square,
|
|
48303
|
-
semantic,
|
|
48304
|
-
edgeClear: !touchesBorder(union, frame),
|
|
48305
|
-
effective: meanConf * containment * size
|
|
48306
|
-
};
|
|
48307
|
-
}
|
|
48308
|
-
function better(a, b) {
|
|
48309
|
-
if (a.visibleCount !== b.visibleCount) return a.visibleCount > b.visibleCount ? a : b;
|
|
48310
|
-
if (a.square !== b.square) return a.square > b.square ? a : b;
|
|
48311
|
-
if (a.semantic !== b.semantic) return a.semantic ? a : b;
|
|
48312
|
-
if (a.edgeClear !== b.edgeClear) return a.edgeClear ? a : b;
|
|
48313
|
-
if (a.effective !== b.effective) return a.effective > b.effective ? a : b;
|
|
48314
|
-
return a.timestamp <= b.timestamp ? a : b;
|
|
48315
|
-
}
|
|
48316
|
-
function chooseGroupInstant(members, frame) {
|
|
48317
|
-
if (members.length === 0) return null;
|
|
48318
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
48319
|
-
for (const m of members) for (const p of m.positions) candidates.add(p.timestamp);
|
|
48320
|
-
let best = null;
|
|
48321
|
-
for (const t of candidates) {
|
|
48322
|
-
const scored = scoreInstant(members, t, frame);
|
|
48323
|
-
if (!scored) continue;
|
|
48324
|
-
best = best === null ? scored : better(scored, best);
|
|
48325
|
-
}
|
|
48326
|
-
if (!best) return null;
|
|
48327
|
-
const w = Math.max(frame.width, 1);
|
|
48328
|
-
const h = Math.max(frame.height, 1);
|
|
48329
|
-
return {
|
|
48330
|
-
timestamp: best.timestamp,
|
|
48331
|
-
union: best.union,
|
|
48332
|
-
ux: best.union.x / w,
|
|
48333
|
-
uy: best.union.y / h,
|
|
48334
|
-
uw: best.union.w / w,
|
|
48335
|
-
uh: best.union.h / h,
|
|
48336
|
-
visibleCount: best.visibleCount
|
|
48337
|
-
};
|
|
48338
|
-
}
|
|
48339
|
-
/** Longest-lived member that owns a clean full frame; ties break on trackId. */
|
|
48340
|
-
function chooseAnchorTrackId(members, ownsCleanFrame) {
|
|
48341
|
-
const eligible = members.filter((m) => ownsCleanFrame(m.trackId));
|
|
48342
|
-
if (eligible.length === 0) return null;
|
|
48343
|
-
return eligible.toSorted((a, b) => {
|
|
48344
|
-
const life = b.lastSeen - b.firstSeen - (a.lastSeen - a.firstSeen);
|
|
48345
|
-
return life !== 0 ? life : a.trackId.localeCompare(b.trackId);
|
|
48346
|
-
})[0].trackId;
|
|
48347
|
-
}
|
|
48348
|
-
/**
|
|
48349
|
-
* The RGB source for a group crop is a full native frame. Thumbnails are
|
|
48350
|
-
* already single-subject crops and cannot substitute for union-bbox geometry.
|
|
48351
|
-
*
|
|
48352
|
-
* Prefer a keyFrame within {@link GROUP_MEDIA_MATCH_MS} of the chosen instant;
|
|
48353
|
-
* if none, take the nearest keyFrame anyway — dropping the crop because the
|
|
48354
|
-
* best-shot was 3 s off the instant is how a recognised person left the
|
|
48355
|
-
* digest with no crop.
|
|
48356
|
-
*/
|
|
48357
|
-
function pickCleanPixelRow(rows, instantMs) {
|
|
48358
|
-
const ranked = (maxDt) => {
|
|
48359
|
-
const candidates = [];
|
|
48360
|
-
for (const row of rows) {
|
|
48361
|
-
const rank = row.kind === "keyFrame" ? 0 : row.kind === "keyFrameSmall" ? 1 : -1;
|
|
48362
|
-
if (rank < 0) continue;
|
|
48363
|
-
const dt = Math.abs(row.timestamp - instantMs);
|
|
48364
|
-
if (maxDt !== null && dt > maxDt) continue;
|
|
48365
|
-
candidates.push({
|
|
48366
|
-
key: row.key,
|
|
48367
|
-
dt,
|
|
48368
|
-
rank
|
|
48369
|
-
});
|
|
48370
|
-
}
|
|
48371
|
-
return candidates.toSorted((a, b) => a.rank - b.rank || a.dt - b.dt)[0];
|
|
48372
|
-
};
|
|
48373
|
-
const near = ranked(GROUP_MEDIA_MATCH_MS);
|
|
48374
|
-
if (near !== void 0) return {
|
|
48375
|
-
key: near.key,
|
|
48376
|
-
usedFallback: false
|
|
48377
|
-
};
|
|
48378
|
-
const any = ranked(null);
|
|
48379
|
-
if (any === void 0) return null;
|
|
48380
|
-
return {
|
|
48381
|
-
key: any.key,
|
|
48382
|
-
usedFallback: true
|
|
48383
|
-
};
|
|
48384
|
-
}
|
|
48385
|
-
//#endregion
|
|
48386
|
-
//#region src/pipeline-analytics/group/group-live-replay.ts
|
|
48387
|
-
function sliceTrackAt(track, now) {
|
|
48388
|
-
const positions = track.positions.filter((p) => p.timestamp <= now);
|
|
48389
|
-
const last = positions[positions.length - 1];
|
|
48390
|
-
return {
|
|
48391
|
-
...track,
|
|
48392
|
-
lastSeen: last?.timestamp ?? track.firstSeen,
|
|
48393
|
-
positions
|
|
48394
|
-
};
|
|
48395
|
-
}
|
|
48396
|
-
function replayInto(tracks, observer) {
|
|
48397
|
-
const events = [];
|
|
48398
|
-
const byId = /* @__PURE__ */ new Map();
|
|
48399
|
-
for (const track of tracks) {
|
|
48400
|
-
byId.set(track.trackId, track);
|
|
48401
|
-
if (track.positions.length === 0) {
|
|
48402
|
-
events.push({
|
|
48403
|
-
t: track.lastSeen,
|
|
48404
|
-
trackId: track.trackId
|
|
48405
|
-
});
|
|
48406
|
-
continue;
|
|
48407
|
-
}
|
|
48408
|
-
for (const p of track.positions) events.push({
|
|
48409
|
-
t: p.timestamp,
|
|
48410
|
-
trackId: track.trackId
|
|
48411
|
-
});
|
|
48412
|
-
}
|
|
48413
|
-
events.sort((a, b) => a.t - b.t || a.trackId.localeCompare(b.trackId));
|
|
48414
|
-
let lastKey = "";
|
|
48415
|
-
for (const e of events) {
|
|
48416
|
-
const key = `${e.trackId}@${e.t}`;
|
|
48417
|
-
if (key === lastKey) continue;
|
|
48418
|
-
lastKey = key;
|
|
48419
|
-
const track = byId.get(e.trackId);
|
|
48420
|
-
if (!track) continue;
|
|
48421
|
-
observer.observe(sliceTrackAt(track, e.t), e.t);
|
|
48422
|
-
}
|
|
48423
|
-
}
|
|
48424
|
-
function replayLive(tracks, assigner = new GroupLiveAssigner()) {
|
|
48425
|
-
replayInto(tracks, assigner);
|
|
48426
|
-
return assigner;
|
|
48427
|
-
}
|
|
48428
|
-
//#endregion
|
|
48429
|
-
//#region src/pipeline-analytics/group/group-sealer.ts
|
|
48430
|
-
var DEFAULT_FRAME = {
|
|
48431
|
-
width: 640,
|
|
48432
|
-
height: 360
|
|
48433
|
-
};
|
|
48434
|
-
var QUERY_LIMIT = 2e3;
|
|
48435
|
-
/** Crash recover: replay this camera only, on its next activity. */
|
|
48436
|
-
var GROUP_CRASH_LOOKBACK_MS = 2 * 6e4;
|
|
48437
|
-
function toInstant(t) {
|
|
48438
|
-
return {
|
|
48439
|
-
trackId: t.trackId,
|
|
48440
|
-
deviceId: t.deviceId,
|
|
48441
|
-
className: t.className,
|
|
48442
|
-
firstSeen: t.firstSeen,
|
|
48443
|
-
lastSeen: t.lastSeen,
|
|
48444
|
-
positions: t.positions,
|
|
48445
|
-
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
|
|
48446
|
-
semantic: t.hasFace === true || t.hasPlate === true,
|
|
48447
|
-
confidence: t.importance
|
|
48448
|
-
};
|
|
48449
|
-
}
|
|
48450
|
-
function memberBoxesAt(members, at, frame) {
|
|
48451
|
-
const w = Math.max(frame.width, 1);
|
|
48452
|
-
const h = Math.max(frame.height, 1);
|
|
48453
|
-
const boxes = [];
|
|
48454
|
-
for (const member of members) {
|
|
48455
|
-
let best = member.positions[0];
|
|
48456
|
-
let bestDt = Number.POSITIVE_INFINITY;
|
|
48457
|
-
for (const p of member.positions) {
|
|
48458
|
-
const dt = Math.abs(p.timestamp - at);
|
|
48459
|
-
if (dt <= 1e3 && dt < bestDt) {
|
|
48460
|
-
best = p;
|
|
48461
|
-
bestDt = dt;
|
|
48462
|
-
}
|
|
48463
|
-
}
|
|
48464
|
-
if (best === void 0 || bestDt === Number.POSITIVE_INFINITY) continue;
|
|
48465
|
-
boxes.push({
|
|
48466
|
-
bbox: {
|
|
48467
|
-
x: best.bbox.x / w,
|
|
48468
|
-
y: best.bbox.y / h,
|
|
48469
|
-
w: best.bbox.w / w,
|
|
48470
|
-
h: best.bbox.h / h
|
|
48471
|
-
},
|
|
48472
|
-
label: member.className,
|
|
48473
|
-
observedAt: best.timestamp
|
|
48474
|
-
});
|
|
48475
|
-
}
|
|
48476
|
-
return boxes;
|
|
48477
|
-
}
|
|
48478
|
-
function toSealer(track) {
|
|
48479
|
-
return {
|
|
48480
|
-
trackId: track.trackId,
|
|
48481
|
-
deviceId: track.deviceId,
|
|
48482
|
-
className: track.className,
|
|
48483
|
-
firstSeen: track.firstSeen,
|
|
48484
|
-
lastSeen: track.lastSeen,
|
|
48485
|
-
positions: track.positions,
|
|
48486
|
-
...track.closedReason !== void 0 ? { closedReason: track.closedReason } : {}
|
|
48487
|
-
};
|
|
48488
|
-
}
|
|
48489
|
-
var GroupSealer = class {
|
|
48490
|
-
deps;
|
|
48491
|
-
constructor(deps) {
|
|
48492
|
-
this.deps = deps;
|
|
48493
|
-
}
|
|
48494
|
-
get processEnabled() {
|
|
48495
|
-
return this.deps.enabled ?? false;
|
|
48496
|
-
}
|
|
48497
|
-
async flushIdle(buffer, now = this.deps.now()) {
|
|
48498
|
-
if (!this.processEnabled) return 0;
|
|
48499
|
-
let written = 0;
|
|
48500
|
-
for (const group of buffer.idleGroups(now, GROUP_SESSION_IDLE_MS)) if (await this.persistLiveGroup(group, buffer)) {
|
|
48501
|
-
buffer.drop([group.id]);
|
|
48502
|
-
written += 1;
|
|
48503
|
-
}
|
|
48504
|
-
return written;
|
|
48505
|
-
}
|
|
48506
|
-
/**
|
|
48507
|
-
* First activity on camera D after a crash: replay 2 minutes of that camera
|
|
48508
|
-
* into the live buffer, persist whoever is already idle, keep the rest in RAM.
|
|
48509
|
-
*/
|
|
48510
|
-
async recoverCamera(deviceId, buffer) {
|
|
48511
|
-
if (!this.processEnabled) return 0;
|
|
48512
|
-
if (buffer.isPrimed(deviceId)) return 0;
|
|
48513
|
-
const now = this.deps.now();
|
|
48514
|
-
try {
|
|
48515
|
-
replayInto(await this.deps.trackStore.queryHistorical({
|
|
48516
|
-
deviceId,
|
|
48517
|
-
since: now - GROUP_CRASH_LOOKBACK_MS,
|
|
48518
|
-
until: now,
|
|
48519
|
-
limit: QUERY_LIMIT,
|
|
48520
|
-
projection: "full"
|
|
48521
|
-
}), buffer);
|
|
48522
|
-
} catch (err) {
|
|
48523
|
-
this.deps.logger.warn("group sealer: lookback failed", {
|
|
48524
|
-
tags: { deviceId },
|
|
48525
|
-
meta: { error: String(err) }
|
|
48526
|
-
});
|
|
48527
|
-
} finally {
|
|
48528
|
-
buffer.markPrimed(deviceId);
|
|
48529
|
-
}
|
|
48530
|
-
return this.flushIdle(buffer, now);
|
|
48531
|
-
}
|
|
48532
|
-
/**
|
|
48533
|
-
* Seal ONE camera over a caller-owned window via live replay.
|
|
48534
|
-
*
|
|
48535
|
-
* This is how a digest recovers groups without a boot scan: the producer
|
|
48536
|
-
* already knows the window. Walking every camera for 48h at initialize()
|
|
48537
|
-
* is what wedged hub-main.
|
|
48538
|
-
*/
|
|
48539
|
-
async sealWindow(deviceId, since, until) {
|
|
48540
|
-
if (!this.processEnabled) return 0;
|
|
48541
|
-
try {
|
|
48542
|
-
const closed = await this.deps.trackStore.queryHistorical({
|
|
48543
|
-
deviceId,
|
|
48544
|
-
since,
|
|
48545
|
-
until,
|
|
48546
|
-
limit: QUERY_LIMIT,
|
|
48547
|
-
projection: "full"
|
|
48548
|
-
});
|
|
48549
|
-
if (closed.length === 0) return 0;
|
|
48550
|
-
const already = new Set((await this.deps.groupStore.membersOfTracks(closed.map((t) => t.trackId))).map((m) => m.trackId));
|
|
48551
|
-
const pending = closed.filter((t) => !already.has(t.trackId));
|
|
48552
|
-
if (pending.length === 0) return 0;
|
|
48553
|
-
const assigner = replayLive(pending);
|
|
48554
|
-
const now = this.deps.now();
|
|
48555
|
-
const byId = new Map(pending.map((t) => [t.trackId, t]));
|
|
48556
|
-
let written = 0;
|
|
48557
|
-
for (const group of assigner.idleGroups(now, GROUP_SESSION_IDLE_MS)) {
|
|
48558
|
-
const members = group.memberTrackIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
|
|
48559
|
-
if (members.length === 0) continue;
|
|
48560
|
-
try {
|
|
48561
|
-
await this.persistPartition(members, group.openedAt);
|
|
48562
|
-
written += 1;
|
|
48563
|
-
} catch (err) {
|
|
48564
|
-
this.deps.logger.warn("group sealer: persist failed", {
|
|
48565
|
-
tags: { deviceId },
|
|
48566
|
-
meta: {
|
|
48567
|
-
members: members.map((m) => m.trackId),
|
|
48568
|
-
error: String(err)
|
|
48569
|
-
}
|
|
48570
|
-
});
|
|
48571
|
-
}
|
|
48572
|
-
}
|
|
48573
|
-
return written;
|
|
48574
|
-
} catch (err) {
|
|
48575
|
-
this.deps.logger.warn("group sealer: device failed", {
|
|
48576
|
-
tags: { deviceId },
|
|
48577
|
-
meta: { error: String(err) }
|
|
48578
|
-
});
|
|
48579
|
-
return 0;
|
|
48580
|
-
}
|
|
48581
|
-
}
|
|
48582
|
-
async persistLiveGroup(group, buffer) {
|
|
48583
|
-
const deviceId = group.deviceId;
|
|
48584
|
-
const snaps = group.memberTrackIds.map((id) => buffer.snapshot(id)).filter((t) => t !== void 0);
|
|
48585
|
-
if (snaps.length === 0) return true;
|
|
48586
|
-
const already = new Set((await this.deps.groupStore.membersOfTracks(snaps.map((t) => t.trackId))).map((m) => m.trackId));
|
|
48587
|
-
const pending = snaps.filter((t) => !already.has(t.trackId)).map(toSealer);
|
|
48588
|
-
if (pending.length === 0) return true;
|
|
48589
|
-
try {
|
|
48590
|
-
await this.persistPartition(pending, group.openedAt);
|
|
48591
|
-
return true;
|
|
48592
|
-
} catch (err) {
|
|
48593
|
-
this.deps.logger.warn("group sealer: persist failed", {
|
|
48594
|
-
tags: { deviceId },
|
|
48595
|
-
meta: {
|
|
48596
|
-
members: pending.map((m) => m.trackId),
|
|
48597
|
-
error: String(err)
|
|
48598
|
-
}
|
|
48599
|
-
});
|
|
48600
|
-
return false;
|
|
48601
|
-
}
|
|
48602
|
-
}
|
|
48603
|
-
async persistPartition(members, openedAt) {
|
|
48604
|
-
const deviceId = members[0].deviceId;
|
|
48605
|
-
const frame = this.deps.frameDims(deviceId) ?? DEFAULT_FRAME;
|
|
48606
|
-
const instantMembers = members.map(toInstant);
|
|
48607
|
-
const instant = chooseGroupInstant(instantMembers, frame);
|
|
48608
|
-
const closedAt = Math.max(...members.map((m) => m.lastSeen));
|
|
48609
|
-
const timestamp = instant?.timestamp ?? closedAt;
|
|
48610
|
-
const mediaInfos = await Promise.all(members.map(async (m) => ({
|
|
48611
|
-
trackId: m.trackId,
|
|
48612
|
-
rows: await this.deps.mediaStore.listInfoByOwner("track", m.trackId)
|
|
48613
|
-
})));
|
|
48614
|
-
const ownsClean = (trackId) => {
|
|
48615
|
-
return mediaInfos.find((m) => m.trackId === trackId)?.rows.some((r) => r.kind === "keyFrame" || r.kind === "keyFrameSmall") ?? false;
|
|
48616
|
-
};
|
|
48617
|
-
const anchorTrackId = members.length >= 2 ? chooseAnchorTrackId(instantMembers, ownsClean) : null;
|
|
48618
|
-
let nativeKey = null;
|
|
48619
|
-
if (anchorTrackId !== null) {
|
|
48620
|
-
const rows = mediaInfos.find((m) => m.trackId === anchorTrackId)?.rows ?? [];
|
|
48621
|
-
nativeKey = (rows.find((r) => r.kind === "keyFrame") ?? rows.find((r) => r.kind === "keyFrameSmall"))?.key ?? null;
|
|
48622
|
-
}
|
|
48623
|
-
const draft = await this.deps.groupStore.insert({
|
|
48624
|
-
deviceId,
|
|
48625
|
-
timestamp,
|
|
48626
|
-
openedAt,
|
|
48627
|
-
closedAt,
|
|
48628
|
-
members: members.map((m) => ({
|
|
48629
|
-
trackId: m.trackId,
|
|
48630
|
-
firstSeen: m.firstSeen
|
|
48631
|
-
})),
|
|
48632
|
-
anchorTrackId,
|
|
48633
|
-
ux: instant?.ux ?? 0,
|
|
48634
|
-
uy: instant?.uy ?? 0,
|
|
48635
|
-
uw: instant?.uw ?? 0,
|
|
48636
|
-
uh: instant?.uh ?? 0,
|
|
48637
|
-
frameWidth: frame.width,
|
|
48638
|
-
frameHeight: frame.height,
|
|
48639
|
-
mediaKey: null,
|
|
48640
|
-
nativeKey
|
|
48641
|
-
});
|
|
48642
|
-
if (members.length < 2 || instant === null) return;
|
|
48643
|
-
const source = await this.pickCleanPixels(mediaInfos, timestamp, deviceId);
|
|
48644
|
-
if (source === null) {
|
|
48645
|
-
this.deps.logger.info("group sealer: no clean pixels for crop", {
|
|
48646
|
-
tags: { deviceId },
|
|
48647
|
-
meta: {
|
|
48648
|
-
groupId: draft.id,
|
|
48649
|
-
members: members.map((m) => m.trackId)
|
|
48650
|
-
}
|
|
48651
|
-
});
|
|
48652
|
-
return;
|
|
48653
|
-
}
|
|
48654
|
-
try {
|
|
48655
|
-
const raw = await (0, sharp.default)(source.jpeg).removeAlpha().toColorspace("srgb").raw().toBuffer({ resolveWithObject: true });
|
|
48656
|
-
if (raw.info.channels === 3) {
|
|
48657
|
-
const union = {
|
|
48658
|
-
x: instant.ux * raw.info.width,
|
|
48659
|
-
y: instant.uy * raw.info.height,
|
|
48660
|
-
w: instant.uw * raw.info.width,
|
|
48661
|
-
h: instant.uh * raw.info.height
|
|
48662
|
-
};
|
|
48663
|
-
const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
|
|
48664
|
-
const mediaKey = await this.deps.mediaStore.put({
|
|
48665
|
-
deviceId,
|
|
48666
|
-
ownerKind: "group",
|
|
48667
|
-
ownerId: draft.id,
|
|
48668
|
-
kind: "thumbnail",
|
|
48669
|
-
timestamp,
|
|
48670
|
-
data: crop
|
|
48671
|
-
});
|
|
48672
|
-
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
48673
|
-
}
|
|
48674
|
-
} catch (err) {
|
|
48675
|
-
this.deps.logger.warn("group sealer: crop failed", {
|
|
48676
|
-
tags: { deviceId },
|
|
48677
|
-
meta: {
|
|
48678
|
-
groupId: draft.id,
|
|
48679
|
-
error: String(err)
|
|
48680
|
-
}
|
|
48681
|
-
});
|
|
48682
|
-
}
|
|
48683
|
-
try {
|
|
48684
|
-
const composed = await composeGroupFrame({
|
|
48685
|
-
jpeg: source.jpeg,
|
|
48686
|
-
boxes: memberBoxesAt(instantMembers, timestamp, frame),
|
|
48687
|
-
at: timestamp,
|
|
48688
|
-
logger: this.deps.logger,
|
|
48689
|
-
deviceId,
|
|
48690
|
-
groupKey: draft.id
|
|
48691
|
-
});
|
|
48692
|
-
if (composed === null) return;
|
|
48693
|
-
await this.deps.mediaStore.put({
|
|
48694
|
-
deviceId,
|
|
48695
|
-
ownerKind: "group",
|
|
48696
|
-
ownerId: draft.id,
|
|
48697
|
-
kind: "fullFrameBoxed",
|
|
48698
|
-
timestamp,
|
|
48699
|
-
data: composed.bytes
|
|
48700
|
-
});
|
|
48701
|
-
} catch (err) {
|
|
48702
|
-
this.deps.logger.warn("group sealer: boxed frame failed", {
|
|
48703
|
-
tags: { deviceId },
|
|
48704
|
-
meta: {
|
|
48705
|
-
groupId: draft.id,
|
|
48706
|
-
error: String(err)
|
|
48707
|
-
}
|
|
48708
|
-
});
|
|
48709
|
-
}
|
|
48710
|
-
}
|
|
48711
|
-
async pickCleanPixels(mediaInfos, instantMs, deviceId) {
|
|
48712
|
-
const chosen = pickCleanPixelRow(mediaInfos.flatMap((info) => info.rows), instantMs);
|
|
48713
|
-
if (chosen === null) return null;
|
|
48714
|
-
if (chosen.usedFallback) this.deps.logger.info("group sealer: crop pixels not within 2s of instant, using nearest keyFrame", {
|
|
48715
|
-
tags: { deviceId },
|
|
48716
|
-
meta: {
|
|
48717
|
-
instantMs,
|
|
48718
|
-
mediaKey: chosen.key
|
|
48719
|
-
}
|
|
48720
|
-
});
|
|
48721
|
-
const file = await this.deps.mediaStore.getByKey(chosen.key);
|
|
48722
|
-
if (file === null) return null;
|
|
48723
|
-
return { jpeg: Buffer.from(file.base64, "base64") };
|
|
48724
|
-
}
|
|
48725
|
-
};
|
|
48726
|
-
//#endregion
|
|
48727
|
-
//#region src/pipeline-analytics/group/group-cursor.ts
|
|
48728
|
-
function encodeGroupCursor(cursor) {
|
|
48729
|
-
return Buffer.from(JSON.stringify({
|
|
48730
|
-
c: cursor.closedAt,
|
|
48731
|
-
i: cursor.groupId
|
|
48732
|
-
}), "utf8").toString("base64url");
|
|
48733
|
-
}
|
|
48734
|
-
function decodeGroupCursor(raw) {
|
|
48735
|
-
let parsed;
|
|
48736
|
-
try {
|
|
48737
|
-
parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
48738
|
-
} catch {
|
|
48739
|
-
throw new Error("listGroups: malformed cursor");
|
|
48740
|
-
}
|
|
48741
|
-
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");
|
|
48742
|
-
return {
|
|
48743
|
-
closedAt: parsed.c,
|
|
48744
|
-
groupId: parsed.i
|
|
48745
|
-
};
|
|
48746
|
-
}
|
|
48747
|
-
function compareGroupDesc(a, b) {
|
|
48748
|
-
if (a.closedAt !== b.closedAt) return b.closedAt - a.closedAt;
|
|
48749
|
-
if (a.id === b.id) return 0;
|
|
48750
|
-
return a.id < b.id ? 1 : -1;
|
|
48751
|
-
}
|
|
48752
|
-
/** True when `row` sits strictly AFTER the cursor in DESC order. */
|
|
48753
|
-
function isAfterGroupCursor(row, cursor) {
|
|
48754
|
-
if (row.closedAt < cursor.closedAt) return true;
|
|
48755
|
-
return row.closedAt === cursor.closedAt && row.id < cursor.groupId;
|
|
48756
|
-
}
|
|
48757
|
-
//#endregion
|
|
48758
|
-
//#region src/pipeline-analytics/group/group-store.ts
|
|
48759
|
-
/**
|
|
48760
|
-
* The group ENTITY store — a co-moving partition of one session, with one
|
|
48761
|
-
* picture of its own (`docs/design/2026-08-21-group-entity-and-group-media.md`).
|
|
48762
|
-
*
|
|
48763
|
-
* Two tables, two questions:
|
|
48764
|
-
*
|
|
48765
|
-
* - `pipeline-analytics:track-groups` is the group row. It has N members and
|
|
48766
|
-
* no `trackId` column; it is owned by its members (owner-indirection).
|
|
48767
|
-
* - `pipeline-analytics:track-group-members` is one row per member. The column
|
|
48768
|
-
* is named exactly `trackId` so the classification derives `track-owned`.
|
|
48769
|
-
*
|
|
48770
|
-
* `anchorTrackId` and `nativeKey` are the only fields mutable after birth:
|
|
48771
|
-
* they are nulled when the anchor member is deleted alone. Everything else is
|
|
48772
|
-
* written once. Re-anchoring is deliberately not done.
|
|
48773
|
-
*
|
|
48774
|
-
* No age clock. A group dies when its last member does; a group with one live
|
|
48775
|
-
* member does not, and it never keeps a dead link.
|
|
48776
|
-
*/
|
|
48777
|
-
/**
|
|
48778
|
-
* @durable class=ledger owner=pipeline-analytics
|
|
48779
|
-
* write="one row per co-moving group, sealed at idle — never a live row.
|
|
48780
|
-
* Membership is the RAM members at persist, not a session partition. mediaKey is
|
|
48781
|
-
* the group's own crop; nativeKey is a POINTER to the anchor member's
|
|
48782
|
-
* keyFrame, nulled if that member is deleted alone. anchorTrackId and
|
|
48783
|
-
* nativeKey are the only fields rewritten after birth."
|
|
48784
|
-
* retention="NO age clock (D61). Rows leave with their last member through
|
|
48785
|
-
* TrackGroupStore.deleteByTracks: memberships first; zero survivors
|
|
48786
|
-
* deletes the group's media then the row; survivors plus a lost anchor
|
|
48787
|
-
* null the pointer. The orphan audit verifies the same via a second
|
|
48788
|
-
* resolver (groupId → any member still present)."
|
|
48789
|
-
*/
|
|
48790
|
-
var TRACK_GROUPS_COLLECTION = "pipeline-analytics:track-groups";
|
|
48791
|
-
/**
|
|
48792
|
-
* @durable class=ledger owner=pipeline-analytics
|
|
48793
|
-
* write="one row per member at group birth — id is '<groupId>:<trackId>'.
|
|
48794
|
-
* timestamp is the member's firstSeen so the orphan audit has a clock
|
|
48795
|
-
* column. Never rewritten."
|
|
48796
|
-
* retention="TRACK-OWNED and clockless (D61): the column is named exactly
|
|
48797
|
-
* trackId, so classification derives ownership and the cascade deletes
|
|
48798
|
-
* the row with its track. A group with surviving members stays."
|
|
48799
|
-
*/
|
|
48800
|
-
var TRACK_GROUP_MEMBERS_COLLECTION = "pipeline-analytics:track-group-members";
|
|
48801
|
-
var TRACK_GROUPS_COLUMNS = [
|
|
48802
|
-
{
|
|
48803
|
-
name: "id",
|
|
48804
|
-
type: "TEXT",
|
|
48805
|
-
primaryKey: true,
|
|
48806
|
-
notNull: true
|
|
48807
|
-
},
|
|
48808
|
-
{
|
|
48809
|
-
name: "deviceId",
|
|
48810
|
-
type: "INTEGER",
|
|
48811
|
-
notNull: true
|
|
48812
|
-
},
|
|
48813
|
-
{
|
|
48814
|
-
name: "timestamp",
|
|
48815
|
-
type: "INTEGER",
|
|
48816
|
-
notNull: true
|
|
48817
|
-
},
|
|
48818
|
-
{
|
|
48819
|
-
name: "openedAt",
|
|
48820
|
-
type: "INTEGER",
|
|
48821
|
-
notNull: true
|
|
48822
|
-
},
|
|
48823
|
-
{
|
|
48824
|
-
name: "closedAt",
|
|
48825
|
-
type: "INTEGER",
|
|
48826
|
-
notNull: true
|
|
48827
|
-
},
|
|
48828
|
-
{
|
|
48829
|
-
name: "memberCount",
|
|
48830
|
-
type: "INTEGER",
|
|
48831
|
-
notNull: true
|
|
48832
|
-
},
|
|
48833
|
-
{
|
|
48834
|
-
name: "anchorTrackId",
|
|
48835
|
-
type: "TEXT"
|
|
48836
|
-
},
|
|
48837
|
-
{
|
|
48838
|
-
name: "ux",
|
|
48839
|
-
type: "REAL",
|
|
48840
|
-
notNull: true
|
|
48841
|
-
},
|
|
48842
|
-
{
|
|
48843
|
-
name: "uy",
|
|
48844
|
-
type: "REAL",
|
|
48845
|
-
notNull: true
|
|
48846
|
-
},
|
|
48847
|
-
{
|
|
48848
|
-
name: "uw",
|
|
48849
|
-
type: "REAL",
|
|
48850
|
-
notNull: true
|
|
48851
|
-
},
|
|
48852
|
-
{
|
|
48853
|
-
name: "uh",
|
|
48854
|
-
type: "REAL",
|
|
48855
|
-
notNull: true
|
|
48856
|
-
},
|
|
48857
|
-
{
|
|
48858
|
-
name: "frameWidth",
|
|
48859
|
-
type: "INTEGER",
|
|
48860
|
-
notNull: true
|
|
48861
|
-
},
|
|
48862
|
-
{
|
|
48863
|
-
name: "frameHeight",
|
|
48864
|
-
type: "INTEGER",
|
|
48865
|
-
notNull: true
|
|
48866
|
-
},
|
|
48867
|
-
{
|
|
48868
|
-
name: "mediaKey",
|
|
48869
|
-
type: "TEXT"
|
|
48870
|
-
},
|
|
48871
|
-
{
|
|
48872
|
-
name: "nativeKey",
|
|
48873
|
-
type: "TEXT"
|
|
48874
|
-
},
|
|
48875
|
-
{
|
|
48876
|
-
name: "sealed",
|
|
48877
|
-
type: "BOOLEAN",
|
|
48878
|
-
notNull: true
|
|
48879
|
-
}
|
|
48880
|
-
];
|
|
48881
|
-
var TRACK_GROUP_MEMBERS_COLUMNS = [
|
|
48882
|
-
{
|
|
48883
|
-
name: "id",
|
|
48884
|
-
type: "TEXT",
|
|
48885
|
-
primaryKey: true,
|
|
48886
|
-
notNull: true
|
|
48887
|
-
},
|
|
48888
|
-
{
|
|
48889
|
-
name: "groupId",
|
|
48890
|
-
type: "TEXT",
|
|
48891
|
-
notNull: true
|
|
48892
|
-
},
|
|
48893
|
-
{
|
|
48894
|
-
name: "trackId",
|
|
48895
|
-
type: "TEXT",
|
|
48896
|
-
notNull: true
|
|
48897
|
-
},
|
|
48898
|
-
{
|
|
48899
|
-
name: "deviceId",
|
|
48900
|
-
type: "INTEGER",
|
|
48901
|
-
notNull: true
|
|
48902
|
-
},
|
|
48903
|
-
{
|
|
48904
|
-
name: "timestamp",
|
|
48905
|
-
type: "INTEGER",
|
|
48906
|
-
notNull: true
|
|
48907
|
-
}
|
|
48908
|
-
];
|
|
48909
|
-
var TRACK_GROUPS_INDEXES = [{
|
|
48910
|
-
name: "idx_track_groups_device_closed",
|
|
48911
|
-
columns: ["deviceId", "closedAt"]
|
|
48912
|
-
}, {
|
|
48913
|
-
name: "idx_track_groups_device_opened",
|
|
48914
|
-
columns: ["deviceId", "openedAt"]
|
|
48915
|
-
}];
|
|
48916
|
-
var TRACK_GROUP_MEMBERS_INDEXES = [{
|
|
48917
|
-
name: "idx_track_group_members_track",
|
|
48918
|
-
columns: ["trackId"]
|
|
48919
|
-
}, {
|
|
48920
|
-
name: "idx_track_group_members_group",
|
|
48921
|
-
columns: ["groupId"]
|
|
48922
|
-
}];
|
|
48923
|
-
function newGroupId() {
|
|
48924
|
-
return `grp-${(0, node_crypto.randomUUID)()}`;
|
|
48925
|
-
}
|
|
48926
|
-
function memberRowId(groupId, trackId) {
|
|
48927
|
-
return `${groupId}:${trackId}`;
|
|
48928
|
-
}
|
|
48929
|
-
var TrackGroupStore = class {
|
|
48930
|
-
store;
|
|
48931
|
-
logger;
|
|
48932
|
-
media;
|
|
48933
|
-
constructor(deps) {
|
|
48934
|
-
this.store = deps.store;
|
|
48935
|
-
this.logger = deps.logger;
|
|
48936
|
-
this.media = deps.media ?? null;
|
|
48937
|
-
}
|
|
48938
|
-
static async declare(store) {
|
|
48939
|
-
await Promise.all([store.declareCollection.mutate({
|
|
48940
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
48941
|
-
columns: [...TRACK_GROUPS_COLUMNS],
|
|
48942
|
-
indexes: [...TRACK_GROUPS_INDEXES]
|
|
48943
|
-
}), store.declareCollection.mutate({
|
|
48944
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
48945
|
-
columns: [...TRACK_GROUP_MEMBERS_COLUMNS],
|
|
48946
|
-
indexes: [...TRACK_GROUP_MEMBERS_INDEXES]
|
|
48947
|
-
})]);
|
|
48948
|
-
}
|
|
48949
|
-
async insert(input) {
|
|
48950
|
-
const id = newGroupId();
|
|
48951
|
-
const group = {
|
|
48952
|
-
id,
|
|
48953
|
-
deviceId: input.deviceId,
|
|
48954
|
-
timestamp: input.timestamp,
|
|
48955
|
-
openedAt: input.openedAt,
|
|
48956
|
-
closedAt: input.closedAt,
|
|
48957
|
-
memberCount: input.members.length,
|
|
48958
|
-
anchorTrackId: input.anchorTrackId,
|
|
48959
|
-
ux: input.ux,
|
|
48960
|
-
uy: input.uy,
|
|
48961
|
-
uw: input.uw,
|
|
48962
|
-
uh: input.uh,
|
|
48963
|
-
frameWidth: input.frameWidth,
|
|
48964
|
-
frameHeight: input.frameHeight,
|
|
48965
|
-
mediaKey: input.mediaKey,
|
|
48966
|
-
nativeKey: input.nativeKey,
|
|
48967
|
-
sealed: true
|
|
48968
|
-
};
|
|
48969
|
-
await this.store.set.mutate({
|
|
48970
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
48971
|
-
key: id,
|
|
48972
|
-
value: this.toRow(group)
|
|
48973
|
-
});
|
|
48974
|
-
for (const member of input.members) await this.store.set.mutate({
|
|
48975
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
48976
|
-
key: memberRowId(id, member.trackId),
|
|
48977
|
-
value: {
|
|
48978
|
-
groupId: id,
|
|
48979
|
-
trackId: member.trackId,
|
|
48980
|
-
deviceId: input.deviceId,
|
|
48981
|
-
timestamp: member.firstSeen
|
|
48982
|
-
}
|
|
48983
|
-
});
|
|
48984
|
-
return group;
|
|
48985
|
-
}
|
|
48986
|
-
/** The crop is written after birth; this is the only post-birth write besides the pointer null. */
|
|
48987
|
-
async setMediaKey(groupId, mediaKey) {
|
|
48988
|
-
const current = await this.get(groupId);
|
|
48989
|
-
if (current === null) return;
|
|
48990
|
-
await this.store.set.mutate({
|
|
48991
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
48992
|
-
key: groupId,
|
|
48993
|
-
value: this.toRow({
|
|
48994
|
-
...current,
|
|
48995
|
-
mediaKey
|
|
48996
|
-
})
|
|
48997
|
-
});
|
|
48998
|
-
}
|
|
48999
|
-
async get(groupId) {
|
|
49000
|
-
const raw = await this.store.get.query({
|
|
49001
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
49002
|
-
key: groupId
|
|
49003
|
-
});
|
|
49004
|
-
if (!raw) return null;
|
|
49005
|
-
return this.normalizeGroup({
|
|
49006
|
-
id: groupId,
|
|
49007
|
-
data: raw
|
|
49008
|
-
});
|
|
49009
|
-
}
|
|
49010
|
-
async listMembers(groupId) {
|
|
49011
|
-
return (await this.store.query.query({
|
|
49012
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
49013
|
-
filter: { where: { groupId } }
|
|
49014
|
-
})).map((r) => this.normalizeMember({
|
|
49015
|
-
id: r.id,
|
|
49016
|
-
data: r.data
|
|
49017
|
-
}));
|
|
49018
|
-
}
|
|
49019
|
-
/**
|
|
49020
|
-
* Membership for a page of groups in ONE query. `listGroupRecords` used to
|
|
49021
|
-
* call {@link listMembers} per row — 60 groups is 60 settings-store RPCs,
|
|
49022
|
-
* ~20 s, and the viewer feed times out into a retry loop.
|
|
49023
|
-
*/
|
|
49024
|
-
async listMembersForGroups(groupIds) {
|
|
49025
|
-
if (groupIds.length === 0) return [];
|
|
49026
|
-
return (await this.store.query.query({
|
|
49027
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
49028
|
-
filter: {
|
|
49029
|
-
whereIn: { groupId: [...groupIds] },
|
|
49030
|
-
limit: 1e4
|
|
49031
|
-
}
|
|
49032
|
-
})).map((r) => this.normalizeMember({
|
|
49033
|
-
id: r.id,
|
|
49034
|
-
data: r.data
|
|
49035
|
-
}));
|
|
49036
|
-
}
|
|
49037
|
-
async membersOfTracks(trackIds) {
|
|
49038
|
-
const out = [];
|
|
49039
|
-
for (const trackId of trackIds) {
|
|
49040
|
-
const rows = await this.store.query.query({
|
|
49041
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
49042
|
-
filter: { where: { trackId } }
|
|
49043
|
-
});
|
|
49044
|
-
for (const r of rows) out.push(this.normalizeMember({
|
|
49045
|
-
id: r.id,
|
|
49046
|
-
data: r.data
|
|
49047
|
-
}));
|
|
49048
|
-
}
|
|
49049
|
-
return out;
|
|
49050
|
-
}
|
|
49051
|
-
/**
|
|
49052
|
-
* Newest-closed page for the Groups feed.
|
|
49053
|
-
*
|
|
49054
|
-
* Time filter is overlap (`closedAt >= since` and `openedAt <= until`),
|
|
49055
|
-
* same as {@link listOverlapping}. Pagination is applied AFTER the window
|
|
49056
|
-
* so a day cursor is honest rather than a raw-row offset that skips live
|
|
49057
|
-
* groups on a busy camera. `deviceIds: []` is an empty page (same as
|
|
49058
|
-
* `listRecentTracks`); omit the field for every camera. `deviceId` stays
|
|
49059
|
-
* as the single-camera alias the custom-action bridge still sends.
|
|
49060
|
-
*/
|
|
49061
|
-
async listRecent(input) {
|
|
49062
|
-
const limit = Math.min(Math.max(input.limit ?? 40, 1), 100);
|
|
49063
|
-
const offset = Math.max(input.offset ?? 0, 0);
|
|
49064
|
-
const deviceIds = input.deviceIds !== void 0 ? input.deviceIds : input.deviceId !== void 0 ? [input.deviceId] : void 0;
|
|
49065
|
-
if (deviceIds !== void 0 && deviceIds.length === 0) return {
|
|
49066
|
-
groups: [],
|
|
49067
|
-
hasMore: false,
|
|
49068
|
-
nextCursor: null
|
|
49069
|
-
};
|
|
49070
|
-
const cursor = input.cursor !== void 0 && input.cursor.length > 0 ? decodeGroupCursor(input.cursor) : null;
|
|
49071
|
-
const filtered = (await this.store.query.query({
|
|
49072
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
49073
|
-
filter: {
|
|
49074
|
-
...deviceIds !== void 0 && deviceIds.length === 1 ? { where: { deviceId: deviceIds[0] } } : deviceIds !== void 0 ? { whereIn: { deviceId: [...deviceIds] } } : {},
|
|
49075
|
-
orderBy: {
|
|
49076
|
-
field: "closedAt",
|
|
49077
|
-
direction: "desc"
|
|
49078
|
-
},
|
|
49079
|
-
limit: 2e3
|
|
49080
|
-
}
|
|
49081
|
-
})).map((r) => this.normalizeGroup({
|
|
49082
|
-
id: r.id,
|
|
49083
|
-
data: r.data
|
|
49084
|
-
})).filter((g) => {
|
|
49085
|
-
if (input.sinceMs !== void 0 && g.closedAt < input.sinceMs) return false;
|
|
49086
|
-
if (input.untilMs !== void 0 && g.openedAt > input.untilMs) return false;
|
|
49087
|
-
if (cursor !== null && !isAfterGroupCursor(g, cursor)) return false;
|
|
49088
|
-
return true;
|
|
49089
|
-
}).toSorted(compareGroupDesc);
|
|
49090
|
-
const slice = (cursor !== null ? filtered : filtered.slice(offset)).slice(0, limit + 1);
|
|
49091
|
-
const groups = slice.slice(0, limit);
|
|
49092
|
-
const hasMore = slice.length > limit;
|
|
49093
|
-
const last = groups[groups.length - 1];
|
|
49094
|
-
return {
|
|
49095
|
-
groups,
|
|
49096
|
-
hasMore,
|
|
49097
|
-
nextCursor: hasMore && last !== void 0 ? encodeGroupCursor({
|
|
49098
|
-
closedAt: last.closedAt,
|
|
49099
|
-
groupId: last.id
|
|
49100
|
-
}) : null
|
|
49101
|
-
};
|
|
49102
|
-
}
|
|
49103
|
-
/** Groups whose life overlaps `[since, until]` on one camera. */
|
|
49104
|
-
async listOverlapping(input) {
|
|
49105
|
-
return (await this.store.query.query({
|
|
49106
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
49107
|
-
filter: {
|
|
49108
|
-
where: { deviceId: input.deviceId },
|
|
49109
|
-
orderBy: {
|
|
49110
|
-
field: "closedAt",
|
|
49111
|
-
direction: "desc"
|
|
49112
|
-
},
|
|
49113
|
-
limit: input.limit ?? 2e3
|
|
49114
|
-
}
|
|
49115
|
-
})).map((r) => this.normalizeGroup({
|
|
49116
|
-
id: r.id,
|
|
49117
|
-
data: r.data
|
|
49118
|
-
})).filter((g) => g.closedAt >= input.sinceMs && g.openedAt <= input.untilMs);
|
|
49119
|
-
}
|
|
49120
|
-
async existingGroupIds(ids) {
|
|
49121
|
-
const live = /* @__PURE__ */ new Set();
|
|
49122
|
-
for (const id of ids) if (await this.store.get.query({
|
|
49123
|
-
collection: "pipeline-analytics:track-groups",
|
|
49124
|
-
key: id
|
|
49125
|
-
})) live.add(id);
|
|
49126
|
-
return live;
|
|
49127
|
-
}
|
|
49128
|
-
/** Group ids that still have at least one membership row. */
|
|
49129
|
-
async groupsWithMembers(ids) {
|
|
49130
|
-
const live = /* @__PURE__ */ new Set();
|
|
49131
|
-
for (const groupId of ids) if ((await this.store.query.query({
|
|
49132
|
-
collection: "pipeline-analytics:track-group-members",
|
|
49133
|
-
filter: {
|
|
49134
|
-
where: { groupId },
|
|
49135
|
-
limit: 1
|
|
49136
|
-
}
|
|
49137
|
-
})).length > 0) live.add(groupId);
|
|
49138
|
-
return live;
|
|
49139
|
-
}
|
|
49140
|
-
/**
|
|
49141
|
-
* Cascade leaf: delete memberships for the batch; a group with zero survivors
|
|
49142
|
-
* dies (media then row); a group that lost only its anchor nulls the pointer.
|
|
49143
|
-
*/
|
|
49144
|
-
async deleteByTracks(trackIds) {
|
|
49145
|
-
const batch = new Set(trackIds);
|
|
49146
|
-
let memberRows = 0;
|
|
49147
|
-
let groupRows = 0;
|
|
49148
|
-
let mediaRows = 0;
|
|
49149
|
-
let mediaBytes = 0;
|
|
49150
|
-
const affected = /* @__PURE__ */ new Set();
|
|
49151
|
-
try {
|
|
49152
|
-
const memberships = await this.membersOfTracks(trackIds);
|
|
49153
|
-
for (const member of memberships) try {
|
|
49154
|
-
await this.store.delete.mutate({
|
|
49155
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
49156
|
-
key: member.id
|
|
49157
|
-
});
|
|
49158
|
-
memberRows += 1;
|
|
49159
|
-
affected.add(member.groupId);
|
|
49160
|
-
} catch (err) {
|
|
49161
|
-
this.logger.warn("TrackGroupStore.deleteByTracks member delete failed", { meta: {
|
|
49162
|
-
memberId: member.id,
|
|
49163
|
-
error: String(err)
|
|
49164
|
-
} });
|
|
49165
|
-
}
|
|
49166
|
-
for (const groupId of affected) try {
|
|
49167
|
-
if ((await this.listMembers(groupId)).length === 0) {
|
|
49168
|
-
if (this.media) {
|
|
49169
|
-
const gone = await this.media.deleteByGroups([groupId]);
|
|
49170
|
-
mediaRows += gone.rows;
|
|
49171
|
-
mediaBytes += gone.bytes;
|
|
49172
|
-
}
|
|
49173
|
-
await this.store.delete.mutate({
|
|
49174
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
49175
|
-
key: groupId
|
|
49176
|
-
});
|
|
49177
|
-
groupRows += 1;
|
|
49178
|
-
continue;
|
|
49179
|
-
}
|
|
49180
|
-
const group = await this.get(groupId);
|
|
49181
|
-
if (group?.anchorTrackId && batch.has(group.anchorTrackId)) await this.store.set.mutate({
|
|
49182
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
49183
|
-
key: groupId,
|
|
49184
|
-
value: this.toRow({
|
|
49185
|
-
...group,
|
|
49186
|
-
anchorTrackId: null,
|
|
49187
|
-
nativeKey: null
|
|
49188
|
-
})
|
|
49189
|
-
});
|
|
49190
|
-
} catch (err) {
|
|
49191
|
-
this.logger.warn("TrackGroupStore.deleteByTracks group update failed", { meta: {
|
|
49192
|
-
groupId,
|
|
49193
|
-
error: String(err)
|
|
49194
|
-
} });
|
|
49195
|
-
}
|
|
49196
|
-
} catch (err) {
|
|
49197
|
-
this.logger.warn("TrackGroupStore.deleteByTracks failed", { meta: { error: String(err) } });
|
|
49198
|
-
}
|
|
49199
|
-
return [{
|
|
49200
|
-
family: "groups",
|
|
49201
|
-
rows: memberRows + groupRows,
|
|
49202
|
-
bytes: 0
|
|
49203
|
-
}, {
|
|
49204
|
-
family: "media",
|
|
49205
|
-
rows: mediaRows,
|
|
49206
|
-
bytes: mediaBytes
|
|
49207
|
-
}];
|
|
49208
|
-
}
|
|
49209
|
-
toRow(group) {
|
|
49210
|
-
return {
|
|
49211
|
-
deviceId: group.deviceId,
|
|
49212
|
-
timestamp: group.timestamp,
|
|
49213
|
-
openedAt: group.openedAt,
|
|
49214
|
-
closedAt: group.closedAt,
|
|
49215
|
-
memberCount: group.memberCount,
|
|
49216
|
-
anchorTrackId: group.anchorTrackId,
|
|
49217
|
-
ux: group.ux,
|
|
49218
|
-
uy: group.uy,
|
|
49219
|
-
uw: group.uw,
|
|
49220
|
-
uh: group.uh,
|
|
49221
|
-
frameWidth: group.frameWidth,
|
|
49222
|
-
frameHeight: group.frameHeight,
|
|
49223
|
-
mediaKey: group.mediaKey,
|
|
49224
|
-
nativeKey: group.nativeKey,
|
|
49225
|
-
sealed: group.sealed
|
|
49226
|
-
};
|
|
49227
|
-
}
|
|
49228
|
-
normalizeGroup(r) {
|
|
49229
|
-
const data = r.data;
|
|
49230
|
-
return {
|
|
49231
|
-
id: r.id,
|
|
49232
|
-
deviceId: Number(data["deviceId"]),
|
|
49233
|
-
timestamp: Number(data["timestamp"]),
|
|
49234
|
-
openedAt: Number(data["openedAt"]),
|
|
49235
|
-
closedAt: Number(data["closedAt"]),
|
|
49236
|
-
memberCount: Number(data["memberCount"]),
|
|
49237
|
-
anchorTrackId: typeof data["anchorTrackId"] === "string" ? data["anchorTrackId"] : null,
|
|
49238
|
-
ux: Number(data["ux"]),
|
|
49239
|
-
uy: Number(data["uy"]),
|
|
49240
|
-
uw: Number(data["uw"]),
|
|
49241
|
-
uh: Number(data["uh"]),
|
|
49242
|
-
frameWidth: Number(data["frameWidth"]),
|
|
49243
|
-
frameHeight: Number(data["frameHeight"]),
|
|
49244
|
-
mediaKey: typeof data["mediaKey"] === "string" ? data["mediaKey"] : null,
|
|
49245
|
-
nativeKey: typeof data["nativeKey"] === "string" ? data["nativeKey"] : null,
|
|
49246
|
-
sealed: Boolean(data["sealed"])
|
|
49247
|
-
};
|
|
49248
|
-
}
|
|
49249
|
-
normalizeMember(r) {
|
|
49250
|
-
const data = r.data;
|
|
49251
|
-
return {
|
|
49252
|
-
id: r.id,
|
|
49253
|
-
groupId: String(data["groupId"]),
|
|
49254
|
-
trackId: String(data["trackId"]),
|
|
49255
|
-
deviceId: Number(data["deviceId"]),
|
|
49256
|
-
timestamp: Number(data["timestamp"])
|
|
49257
|
-
};
|
|
49258
|
-
}
|
|
49259
|
-
};
|
|
49260
47848
|
//#endregion
|
|
49261
47849
|
//#region src/pipeline-analytics/media-capture-log.ts
|
|
49262
47850
|
var DEFAULT_FLUSH_INTERVAL_MS$1 = 6e4;
|
|
@@ -57663,24 +56251,12 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
57663
56251
|
collection: PLATES_COLLECTION,
|
|
57664
56252
|
classification: derived(PLATE_COLUMNS)
|
|
57665
56253
|
},
|
|
57666
|
-
{
|
|
57667
|
-
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
57668
|
-
classification: derived(TRACK_GROUP_MEMBERS_COLUMNS)
|
|
57669
|
-
},
|
|
57670
56254
|
{
|
|
57671
56255
|
collection: MEDIA_COLLECTION,
|
|
57672
56256
|
classification: {
|
|
57673
56257
|
kind: "owner-indirection",
|
|
57674
56258
|
derivation: "explicit",
|
|
57675
|
-
why: "(ownerKind, ownerId) means a different entity per kind — a trackId for 'track', a PREFIXED trackId for 'face'/'plate', an eventId for 'event',
|
|
57676
|
-
}
|
|
57677
|
-
},
|
|
57678
|
-
{
|
|
57679
|
-
collection: TRACK_GROUPS_COLLECTION,
|
|
57680
|
-
classification: {
|
|
57681
|
-
kind: "owner-indirection",
|
|
57682
|
-
derivation: "explicit",
|
|
57683
|
-
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."
|
|
56259
|
+
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."
|
|
57684
56260
|
}
|
|
57685
56261
|
},
|
|
57686
56262
|
{
|
|
@@ -57792,7 +56368,7 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
57792
56368
|
collection: SCENE_LATCH_COLLECTION,
|
|
57793
56369
|
classification: {
|
|
57794
56370
|
kind: "never-orphaned",
|
|
57795
|
-
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`
|
|
56371
|
+
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`)."
|
|
57796
56372
|
}
|
|
57797
56373
|
},
|
|
57798
56374
|
{
|
|
@@ -57852,6 +56428,10 @@ function resolveMediaOwner(row) {
|
|
|
57852
56428
|
kind: "unresolvable",
|
|
57853
56429
|
why: "media row carries no (ownerKind, ownerId)"
|
|
57854
56430
|
};
|
|
56431
|
+
if (isRetiredMediaOwnerKind(kindRaw)) return {
|
|
56432
|
+
kind: "retired-owner",
|
|
56433
|
+
ownerKind: kindRaw
|
|
56434
|
+
};
|
|
57855
56435
|
const ownerKind = kindRaw;
|
|
57856
56436
|
if (isRetentionExemptOwnerKind(ownerKind)) return {
|
|
57857
56437
|
kind: "exempt",
|
|
@@ -57880,10 +56460,6 @@ function resolveMediaOwner(row) {
|
|
|
57880
56460
|
kind: "unresolvable",
|
|
57881
56461
|
why: `plate-owned media whose ownerId does not carry the '${PLATE_MEDIA_OWNER_PREFIX}' prefix`
|
|
57882
56462
|
};
|
|
57883
|
-
case "group": return {
|
|
57884
|
-
kind: "group",
|
|
57885
|
-
groupId: ownerId
|
|
57886
|
-
};
|
|
57887
56463
|
default: return {
|
|
57888
56464
|
kind: "unresolvable",
|
|
57889
56465
|
why: `unknown ownerKind '${kindRaw}'`
|
|
@@ -57895,13 +56471,6 @@ function resolveMediaOwner(row) {
|
|
|
57895
56471
|
* is derived the same way the classification is — an enrolled row is out of
|
|
57896
56472
|
* reach regardless of what happened to the track that produced it.
|
|
57897
56473
|
*/
|
|
57898
|
-
/** A group row lives iff any membership row still names it. */
|
|
57899
|
-
function resolveGroupOwner(row) {
|
|
57900
|
-
return {
|
|
57901
|
-
kind: "group-members",
|
|
57902
|
-
groupId: row.key
|
|
57903
|
-
};
|
|
57904
|
-
}
|
|
57905
56474
|
function resolveDeclaredTrackOwner(row, enrolmentExempt) {
|
|
57906
56475
|
if (enrolmentExempt && row.data["assigned"] === true) return {
|
|
57907
56476
|
kind: "exempt",
|
|
@@ -57932,17 +56501,6 @@ function buildOrphanScopes(collections = auditableCollections()) {
|
|
|
57932
56501
|
});
|
|
57933
56502
|
continue;
|
|
57934
56503
|
}
|
|
57935
|
-
if (entry.collection === "pipeline-analytics:track-groups") {
|
|
57936
|
-
scopes.push({
|
|
57937
|
-
id: entry.collection,
|
|
57938
|
-
collection: entry.collection,
|
|
57939
|
-
resolve: resolveGroupOwner,
|
|
57940
|
-
timestampField: "timestamp",
|
|
57941
|
-
sizeField: null,
|
|
57942
|
-
deviceIdField: "deviceId"
|
|
57943
|
-
});
|
|
57944
|
-
continue;
|
|
57945
|
-
}
|
|
57946
56504
|
continue;
|
|
57947
56505
|
}
|
|
57948
56506
|
if (entry.classification.kind !== "track-owned") continue;
|
|
@@ -57974,17 +56532,11 @@ var DEFAULT_ORPHAN_LIMITS = {
|
|
|
57974
56532
|
function questionsFor(refs) {
|
|
57975
56533
|
const trackIds = /* @__PURE__ */ new Set();
|
|
57976
56534
|
const eventIds = /* @__PURE__ */ new Set();
|
|
57977
|
-
const groupIds = /* @__PURE__ */ new Set();
|
|
57978
|
-
const groupMemberIds = /* @__PURE__ */ new Set();
|
|
57979
56535
|
for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
|
|
57980
56536
|
else if (ref.kind === "event") eventIds.add(ref.eventId);
|
|
57981
|
-
else if (ref.kind === "group") groupIds.add(ref.groupId);
|
|
57982
|
-
else if (ref.kind === "group-members") groupMemberIds.add(ref.groupId);
|
|
57983
56537
|
return {
|
|
57984
56538
|
trackIds: [...trackIds],
|
|
57985
|
-
eventIds: [...eventIds]
|
|
57986
|
-
groupIds: [...groupIds],
|
|
57987
|
-
groupMemberIds: [...groupMemberIds]
|
|
56539
|
+
eventIds: [...eventIds]
|
|
57988
56540
|
};
|
|
57989
56541
|
}
|
|
57990
56542
|
/**
|
|
@@ -58016,16 +56568,12 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
58016
56568
|
break;
|
|
58017
56569
|
}
|
|
58018
56570
|
const refs = rows.map((row) => scope.resolve(row));
|
|
58019
|
-
const { trackIds, eventIds
|
|
56571
|
+
const { trackIds, eventIds } = questionsFor(refs);
|
|
58020
56572
|
let liveTracks;
|
|
58021
56573
|
let liveEvents;
|
|
58022
|
-
let liveGroups;
|
|
58023
|
-
let liveGroupsWithMembers;
|
|
58024
56574
|
try {
|
|
58025
56575
|
liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
|
|
58026
56576
|
liveEvents = eventIds.length > 0 ? await deps.liveEventIds(eventIds) : /* @__PURE__ */ new Set();
|
|
58027
|
-
liveGroups = groupIds.length > 0 && deps.liveGroupIds ? await deps.liveGroupIds(groupIds) : /* @__PURE__ */ new Set();
|
|
58028
|
-
liveGroupsWithMembers = groupMemberIds.length > 0 && deps.liveGroupsWithMembers ? await deps.liveGroupsWithMembers(groupMemberIds) : /* @__PURE__ */ new Set();
|
|
58029
56577
|
} catch (err) {
|
|
58030
56578
|
deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
|
|
58031
56579
|
collection: scope.collection,
|
|
@@ -58049,7 +56597,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
58049
56597
|
tally.unresolvable += 1;
|
|
58050
56598
|
continue;
|
|
58051
56599
|
}
|
|
58052
|
-
if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) :
|
|
56600
|
+
if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) : false) continue;
|
|
58053
56601
|
const ts = num(row.data, scope.timestampField);
|
|
58054
56602
|
if (ts !== null && ts > graceFloor) {
|
|
58055
56603
|
tally.withinGrace += 1;
|
|
@@ -64887,8 +63435,7 @@ var customActions = {
|
|
|
64887
63435
|
...locationDiscardActions,
|
|
64888
63436
|
...viewerSettingsActions,
|
|
64889
63437
|
...photoEnrollActions,
|
|
64890
|
-
...faceRescoreActions
|
|
64891
|
-
...groupActions
|
|
63438
|
+
...faceRescoreActions
|
|
64892
63439
|
};
|
|
64893
63440
|
/**
|
|
64894
63441
|
* Assist threshold when the caller does not pin one. Packages have their own
|
|
@@ -65180,11 +63727,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65180
63727
|
faceStore = null;
|
|
65181
63728
|
faceRecognizer = null;
|
|
65182
63729
|
plateStore = null;
|
|
65183
|
-
groupStore = null;
|
|
65184
|
-
groupSealer = null;
|
|
65185
|
-
groupLive = null;
|
|
65186
|
-
/** Serialise first-activity lookback per camera, then observe in order. */
|
|
65187
|
-
groupObserveTail = /* @__PURE__ */ new Map();
|
|
65188
63730
|
plateRecognizer = null;
|
|
65189
63731
|
objectEmbeddingStore = null;
|
|
65190
63732
|
/** Live state of the detached embedding rebuild — see `rebuildObjectEmbeddings`. */
|
|
@@ -66142,8 +64684,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66142
64684
|
...locationDiscardActions,
|
|
66143
64685
|
...viewerSettingsActions,
|
|
66144
64686
|
...photoEnrollActions,
|
|
66145
|
-
...faceRescoreActions
|
|
66146
|
-
...groupActions
|
|
64687
|
+
...faceRescoreActions
|
|
66147
64688
|
},
|
|
66148
64689
|
actionHandlers: {
|
|
66149
64690
|
...ncHandlers,
|
|
@@ -66151,7 +64692,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66151
64692
|
...this.buildDebugActionHandlers(),
|
|
66152
64693
|
...this.buildPhotoEnrollActionHandlers(),
|
|
66153
64694
|
...this.buildFaceRescoreActionHandlers(),
|
|
66154
|
-
...this.buildGroupActionHandlers(),
|
|
66155
64695
|
...this.viewerSettingsSnapshots ? makeViewerSettingsActionHandlers(this.viewerSettingsSnapshots) : {}
|
|
66156
64696
|
}
|
|
66157
64697
|
} : {}
|
|
@@ -66385,7 +64925,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66385
64925
|
SceneStore.declare(api.settingsStore),
|
|
66386
64926
|
NotificationCenter.declare(api.settingsStore),
|
|
66387
64927
|
ViewerSettingsSnapshotStore.declare(api.settingsStore),
|
|
66388
|
-
TrackGroupStore.declare(api.settingsStore),
|
|
66389
64928
|
declareMigrationMarkers(api.settingsStore)
|
|
66390
64929
|
]);
|
|
66391
64930
|
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
@@ -66745,31 +65284,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66745
65284
|
logger: logger.child("PlateStore")
|
|
66746
65285
|
});
|
|
66747
65286
|
this.plateStore = plateStore;
|
|
66748
|
-
const vehicleStore = new VehicleStore({
|
|
66749
|
-
store: api.settingsStore,
|
|
66750
|
-
logger: logger.child("VehicleStore")
|
|
66751
|
-
});
|
|
66752
|
-
const groupStore = new TrackGroupStore({
|
|
66753
|
-
store: api.settingsStore,
|
|
66754
|
-
logger: logger.child("TrackGroupStore"),
|
|
66755
|
-
media: mediaStore
|
|
66756
|
-
});
|
|
66757
|
-
this.groupStore = groupStore;
|
|
66758
|
-
this.groupLive = new GroupLiveBuffer();
|
|
66759
|
-
this.groupSealer = new GroupSealer({
|
|
66760
|
-
trackStore,
|
|
66761
|
-
groupStore,
|
|
66762
|
-
mediaStore,
|
|
66763
|
-
logger: logger.child("GroupSealer"),
|
|
66764
|
-
frameDims: (deviceId) => {
|
|
66765
|
-
const dims = this.lastFrameDimsByDevice.get(deviceId);
|
|
66766
|
-
return dims ? {
|
|
66767
|
-
width: dims.w,
|
|
66768
|
-
height: dims.h
|
|
66769
|
-
} : void 0;
|
|
66770
|
-
},
|
|
66771
|
-
now: () => Date.now()
|
|
66772
|
-
});
|
|
66773
65287
|
return {
|
|
66774
65288
|
trackStore,
|
|
66775
65289
|
mediaStore,
|
|
@@ -66777,8 +65291,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66777
65291
|
identityStore,
|
|
66778
65292
|
faceStore,
|
|
66779
65293
|
plateStore,
|
|
66780
|
-
vehicleStore
|
|
66781
|
-
|
|
65294
|
+
vehicleStore: new VehicleStore({
|
|
65295
|
+
store: api.settingsStore,
|
|
65296
|
+
logger: logger.child("VehicleStore")
|
|
65297
|
+
})
|
|
66782
65298
|
};
|
|
66783
65299
|
}
|
|
66784
65300
|
/** Face + plate recognizers, the durable keyFrame-derived plate crop and the
|
|
@@ -67299,28 +65815,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
67299
65815
|
}));
|
|
67300
65816
|
},
|
|
67301
65817
|
listMediaInfo: async (id) => {
|
|
67302
|
-
const group = await stores.groupStore.get(id);
|
|
67303
|
-
if (group) {
|
|
67304
|
-
if (group.mediaKey || group.nativeKey) return digestMediaRows({
|
|
67305
|
-
mediaKey: group.mediaKey,
|
|
67306
|
-
nativeKey: group.nativeKey,
|
|
67307
|
-
timestamp: group.timestamp,
|
|
67308
|
-
memberRows: []
|
|
67309
|
-
});
|
|
67310
|
-
const members = await stores.groupStore.listMembers(group.id);
|
|
67311
|
-
const memberRows = (await Promise.all(members.map((m) => stores.mediaStore.listInfoByOwner("track", m.trackId)))).flatMap((rows) => rows.map((r) => ({
|
|
67312
|
-
key: r.key,
|
|
67313
|
-
kind: r.kind,
|
|
67314
|
-
timestamp: r.timestamp,
|
|
67315
|
-
sizeBytes: r.sizeBytes
|
|
67316
|
-
})));
|
|
67317
|
-
return digestMediaRows({
|
|
67318
|
-
mediaKey: group.mediaKey,
|
|
67319
|
-
nativeKey: group.nativeKey,
|
|
67320
|
-
timestamp: group.timestamp,
|
|
67321
|
-
memberRows
|
|
67322
|
-
});
|
|
67323
|
-
}
|
|
67324
65818
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
67325
65819
|
key: r.key,
|
|
67326
65820
|
kind: r.kind,
|
|
@@ -68691,7 +67185,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
68691
67185
|
state: t.state
|
|
68692
67186
|
});
|
|
68693
67187
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
68694
|
-
this.enqueueLiveGroup(upserted, result.timestamp);
|
|
68695
67188
|
}
|
|
68696
67189
|
if (result.rawTrackedDetections.length > 0) {
|
|
68697
67190
|
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
@@ -70788,11 +69281,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70788
69281
|
async sweepExpiredTracks() {
|
|
70789
69282
|
if (this.shuttingDown || !this.trackStore) return;
|
|
70790
69283
|
await this.trackCloser.sweep();
|
|
70791
|
-
if (this.isPostProcessingNode && this.groupLive) this.groupSealer?.flushIdle(this.groupLive);
|
|
70792
69284
|
}
|
|
70793
|
-
/** Hub-only: assign this sighting in RAM. First activity on a camera
|
|
70794
|
-
* replays 2 minutes of that camera (crash recover), then observe is sync. */
|
|
70795
|
-
enqueueLiveGroup(track, now) {}
|
|
70796
69285
|
/** Unified retention (Phase 1): earliest retained footage per device — the
|
|
70797
69286
|
* follow-recordings cutoff source. Cached 10 min: availability answers in
|
|
70798
69287
|
* ~70 ms but the sweep asks once per device per pass. `null` = no footage
|
|
@@ -71494,16 +69983,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71494
69983
|
};
|
|
71495
69984
|
if (plausible) this.stationaryRegistry?.promote(entry);
|
|
71496
69985
|
processor.dropTrack(track.trackId);
|
|
71497
|
-
const peek = this.trackStore?.peekActive(track.trackId);
|
|
71498
|
-
if (peek) this.enqueueLiveGroup(toGroupableTrack({
|
|
71499
|
-
trackId: track.trackId,
|
|
71500
|
-
deviceId,
|
|
71501
|
-
className: track.className,
|
|
71502
|
-
firstSeen: peek.firstSeen,
|
|
71503
|
-
lastSeen: timestamp,
|
|
71504
|
-
positions: peek.positions,
|
|
71505
|
-
closedReason: "stationary-promotion"
|
|
71506
|
-
}), timestamp);
|
|
71507
69986
|
this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
|
|
71508
69987
|
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
71509
69988
|
tags: { deviceId },
|
|
@@ -71731,15 +70210,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71731
70210
|
async listRecentTracks(input) {
|
|
71732
70211
|
return this.queryFacade.listRecentTracks(input);
|
|
71733
70212
|
}
|
|
71734
|
-
async listGroups(input) {
|
|
71735
|
-
return {
|
|
71736
|
-
groups: [],
|
|
71737
|
-
nextCursor: null
|
|
71738
|
-
};
|
|
71739
|
-
}
|
|
71740
|
-
async getGroup(input) {
|
|
71741
|
-
return null;
|
|
71742
|
-
}
|
|
71743
70213
|
/**
|
|
71744
70214
|
* Every event kind the device can produce: built-ins (motion + audio),
|
|
71745
70215
|
* detection classes actually observed on the device, and sensor kinds
|
|
@@ -72258,8 +70728,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
72258
70728
|
},
|
|
72259
70729
|
liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
|
|
72260
70730
|
liveEventIds: (ids) => eventStore.existingEventIds(ids),
|
|
72261
|
-
liveGroupIds: (ids) => this.groupStore?.existingGroupIds(ids) ?? Promise.resolve(/* @__PURE__ */ new Set()),
|
|
72262
|
-
liveGroupsWithMembers: (ids) => this.groupStore?.groupsWithMembers(ids) ?? Promise.resolve(/* @__PURE__ */ new Set()),
|
|
72263
70731
|
reclaim: async (collection, key) => {
|
|
72264
70732
|
if (collection === "pipeline-analytics:media") {
|
|
72265
70733
|
await mediaStore.deleteByKey(key);
|
|
@@ -73022,41 +71490,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73022
71490
|
buildFaceRescoreActionHandlers() {
|
|
73023
71491
|
return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
|
|
73024
71492
|
}
|
|
73025
|
-
groupQueryPorts() {
|
|
73026
|
-
const groupStore = this.groupStore;
|
|
73027
|
-
if (groupStore === null) return null;
|
|
73028
|
-
return {
|
|
73029
|
-
groupStore,
|
|
73030
|
-
loadTracks: async (trackIds) => {
|
|
73031
|
-
const map = /* @__PURE__ */ new Map();
|
|
73032
|
-
const tracks = this.trackStore;
|
|
73033
|
-
if (!tracks || trackIds.length === 0) return map;
|
|
73034
|
-
const missing = [];
|
|
73035
|
-
for (const id of trackIds) {
|
|
73036
|
-
const active = tracks.getActiveByTrack(id);
|
|
73037
|
-
if (active !== null) map.set(id, trackToLite(active));
|
|
73038
|
-
else missing.push(id);
|
|
73039
|
-
}
|
|
73040
|
-
if (missing.length === 0) return map;
|
|
73041
|
-
for (const persisted of await tracks.getPersistedByTrackIds(missing)) map.set(persisted.trackId, trackToLite(persisted));
|
|
73042
|
-
return map;
|
|
73043
|
-
},
|
|
73044
|
-
eventMediaBaseUrl: () => this.eventMediaBaseUrl
|
|
73045
|
-
};
|
|
73046
|
-
}
|
|
73047
|
-
buildGroupActionHandlers() {
|
|
73048
|
-
return {
|
|
73049
|
-
"analytics.listGroups": async (input) => {
|
|
73050
|
-
return {
|
|
73051
|
-
groups: [],
|
|
73052
|
-
hasMore: false
|
|
73053
|
-
};
|
|
73054
|
-
},
|
|
73055
|
-
"analytics.getGroup": async (input) => {
|
|
73056
|
-
return { group: null };
|
|
73057
|
-
}
|
|
73058
|
-
};
|
|
73059
|
-
}
|
|
73060
71493
|
buildPhotoEnrollActionHandlers() {
|
|
73061
71494
|
return {
|
|
73062
71495
|
"photo.analyze": async (input) => {
|
|
@@ -73442,8 +71875,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73442
71875
|
mediaStore,
|
|
73443
71876
|
faceStore: this.faceStore,
|
|
73444
71877
|
plateStore: this.plateStore,
|
|
73445
|
-
objectEmbeddingStore: this.objectEmbeddingStore
|
|
73446
|
-
groupStore: this.groupStore
|
|
71878
|
+
objectEmbeddingStore: this.objectEmbeddingStore
|
|
73447
71879
|
};
|
|
73448
71880
|
const absent = absentCascadeLegs(stores);
|
|
73449
71881
|
if (absent.length > 0) this.ctx.logger.warn("track cascade is missing a leg — its rows will be left behind", { meta: {
|
|
@@ -73652,8 +72084,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73652
72084
|
media: 0,
|
|
73653
72085
|
faces: 0,
|
|
73654
72086
|
plates: 0,
|
|
73655
|
-
embeddings: 0
|
|
73656
|
-
groups: 0
|
|
72087
|
+
embeddings: 0
|
|
73657
72088
|
};
|
|
73658
72089
|
this.lastPruneBytes = 0;
|
|
73659
72090
|
const cascade = this.buildTrackCascadeRegistry();
|
|
@@ -73713,8 +72144,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73713
72144
|
media: tally.media,
|
|
73714
72145
|
faces: tally.faces,
|
|
73715
72146
|
plates: tally.plates,
|
|
73716
|
-
embeddings: tally.embeddings
|
|
73717
|
-
groups: tally.groups
|
|
72147
|
+
embeddings: tally.embeddings
|
|
73718
72148
|
};
|
|
73719
72149
|
}
|
|
73720
72150
|
/**
|