@camstack/addon-post-analysis 1.2.127 → 1.2.129
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-BDy0xpfI.mjs → dist-CGhP6T_c.mjs} +2536 -2281
- package/dist/{dist-BarcMD8R.js → dist-DFeeUt_t.js} +2536 -2281
- 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-Dj-k47_c.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D4G4SLMY.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-Kj0oid5Z.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C2KWpXJG.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-XLZIUPmq.mjs → hostInit-TOYw2CMN.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +465 -12
- package/dist/pipeline-analytics/index.mjs +465 -12
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-DFeeUt_t.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");
|
|
@@ -27217,6 +27217,18 @@ var FaceGalleryProvider = class {
|
|
|
27217
27217
|
* This module names that state (`model-mismatch`) so the UI can say "this face
|
|
27218
27218
|
* is in an old feature space" instead of showing 0% and letting the operator
|
|
27219
27219
|
* conclude the person is not there.
|
|
27220
|
+
*
|
|
27221
|
+
* ## `buildFaceRescoreReport` stays pure — the CALLER persists
|
|
27222
|
+
*
|
|
27223
|
+
* This function never touches the store; every input is passed in and nothing
|
|
27224
|
+
* is read or written (see its own doc below). The operator asked for this
|
|
27225
|
+
* exact recompute, though, and wants the answer to stick — so the ACTION that
|
|
27226
|
+
* wraps this report (`rescoreFaceForTrack` in `pipeline-analytics/index.ts`)
|
|
27227
|
+
* writes the result onto `Face.suggestedIdentityId` / `suggestedMatchScore`,
|
|
27228
|
+
* through `decideRescorePersistence` / `persistRescoreResult` below. That is
|
|
27229
|
+
* still not a background job — it is the SAME explicitly-requested call,
|
|
27230
|
+
* writing once. A `model-mismatch` report (`comparable: false`) is never
|
|
27231
|
+
* written: see `persistRescoreResult`.
|
|
27220
27232
|
*/
|
|
27221
27233
|
/** The empty report every "cannot score" path returns, so shape never varies. */
|
|
27222
27234
|
function emptyReport(verdict, input, comparable) {
|
|
@@ -27292,6 +27304,52 @@ function buildFaceRescoreReport(input) {
|
|
|
27292
27304
|
...best !== void 0 && second !== void 0 ? { runnerUpGap: best.score - second.score } : {}
|
|
27293
27305
|
};
|
|
27294
27306
|
}
|
|
27307
|
+
/**
|
|
27308
|
+
* What `face.rescoreTrack` should PERSIST for a COMPARABLE report, decided as
|
|
27309
|
+
* its own pure function so the rule is directly testable — separated from the
|
|
27310
|
+
* class that owns the store I/O (`rescoreFaceForTrack` in
|
|
27311
|
+
* `pipeline-analytics/index.ts`), which only wires this answer to
|
|
27312
|
+
* `FaceStore.setSuggestion`.
|
|
27313
|
+
*
|
|
27314
|
+
* `null` covers every verdict OTHER than `auto-assignable` / `suggestion`, and
|
|
27315
|
+
* also an `auto-assignable` / `suggestion` verdict whose best-ranked row is
|
|
27316
|
+
* not itself ELIGIBLE (`identities` is ranked by raw score across every
|
|
27317
|
+
* comparable identity, eligible or not — see `buildFaceRescoreReport`). Every
|
|
27318
|
+
* one of those is a genuine "nothing to suggest right now": never an
|
|
27319
|
+
* assignment, and never the winner of an `ambiguous` verdict — the matcher
|
|
27320
|
+
* refused to pick one, so this function does not pick one either.
|
|
27321
|
+
*
|
|
27322
|
+
* Callers MUST gate on `report.comparable` before calling this — a
|
|
27323
|
+
* `model-mismatch` report has no verdict worth acting on; see
|
|
27324
|
+
* {@link persistRescoreResult}, which does that gating and is the one thing
|
|
27325
|
+
* `rescoreFaceForTrack` actually calls.
|
|
27326
|
+
*/
|
|
27327
|
+
function decideRescorePersistence(report) {
|
|
27328
|
+
if (report.verdict !== "auto-assignable" && report.verdict !== "suggestion") return null;
|
|
27329
|
+
const candidate = report.identities.find((row) => row.eligible);
|
|
27330
|
+
return candidate === void 0 ? null : {
|
|
27331
|
+
identityId: candidate.identityId,
|
|
27332
|
+
score: candidate.score
|
|
27333
|
+
};
|
|
27334
|
+
}
|
|
27335
|
+
/**
|
|
27336
|
+
* Persist `face.rescoreTrack`'s result onto the SUGGESTED tier — the ONE
|
|
27337
|
+
* write this explicitly-requested action makes, and the only place that
|
|
27338
|
+
* decides whether it happens at all.
|
|
27339
|
+
*
|
|
27340
|
+
* A non-comparable report (`model-mismatch`) short-circuits BEFORE
|
|
27341
|
+
* `ports.setSuggestion` is ever called: a cosine across model spaces is
|
|
27342
|
+
* well-formed noise, and persisting it would look exactly like a fresh,
|
|
27343
|
+
* meaningful read to every future reader of the row. This is the single most
|
|
27344
|
+
* load-bearing branch in this file — the regression it guards against writes
|
|
27345
|
+
* garbage that reads as data.
|
|
27346
|
+
*/
|
|
27347
|
+
async function persistRescoreResult(faceId, report, ports) {
|
|
27348
|
+
if (!report.comparable) return "not-comparable";
|
|
27349
|
+
const candidate = decideRescorePersistence(report);
|
|
27350
|
+
await ports.setSuggestion(faceId, candidate);
|
|
27351
|
+
return candidate !== null ? "written" : "cleared";
|
|
27352
|
+
}
|
|
27295
27353
|
//#endregion
|
|
27296
27354
|
//#region src/pipeline-analytics/face-rescore-actions.ts
|
|
27297
27355
|
/**
|
|
@@ -27392,8 +27450,10 @@ var FaceRescoreResultSchema = require_dist.object({
|
|
|
27392
27450
|
/** The face's current assignment, so the UI can mark "already this person". */
|
|
27393
27451
|
assigned: require_dist.boolean(),
|
|
27394
27452
|
assignedIdentityId: require_dist.string().optional(),
|
|
27395
|
-
/** What the track has been SHOWING until now —
|
|
27396
|
-
*
|
|
27453
|
+
/** What the track has been SHOWING until now — read BEFORE this call
|
|
27454
|
+
* writes, so it is whatever a previous track close or a previous
|
|
27455
|
+
* `face.rescoreTrack` last persisted. Returned so the UI can put the stale
|
|
27456
|
+
* number beside the fresh one. */
|
|
27397
27457
|
storedSuggestedIdentityId: require_dist.string().optional(),
|
|
27398
27458
|
storedSuggestedMatchScore: require_dist.number().optional(),
|
|
27399
27459
|
verdict: FaceRescoreVerdictSchema,
|
|
@@ -27410,9 +27470,12 @@ var FaceRescoreResultSchema = require_dist.object({
|
|
|
27410
27470
|
});
|
|
27411
27471
|
var faceRescoreActions = require_dist.defineCustomActions({
|
|
27412
27472
|
/**
|
|
27413
|
-
*
|
|
27414
|
-
*
|
|
27415
|
-
*
|
|
27473
|
+
* Persists its own result onto the SUGGESTED tier (`suggestedIdentityId` /
|
|
27474
|
+
* `suggestedMatchScore`) — never an assignment, never automatic; see
|
|
27475
|
+
* `rescoreFaceForTrack` in `pipeline-analytics/index.ts` and
|
|
27476
|
+
* `persistRescoreResult` in `face-rescore.ts`. `admin` because the report
|
|
27477
|
+
* also names every enrolled person and how well an arbitrary face matches
|
|
27478
|
+
* them — the gallery's shape, which is not a viewer-level fact.
|
|
27416
27479
|
*/
|
|
27417
27480
|
"face.rescoreTrack": require_dist.customAction(FaceRescoreInputSchema, FaceRescoreResultSchema, {
|
|
27418
27481
|
kind: "mutation",
|
|
@@ -29407,6 +29470,51 @@ var FrameProcessor = class {
|
|
|
29407
29470
|
}
|
|
29408
29471
|
};
|
|
29409
29472
|
//#endregion
|
|
29473
|
+
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
29474
|
+
/**
|
|
29475
|
+
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
29476
|
+
* return the tracks it produced.
|
|
29477
|
+
*
|
|
29478
|
+
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
29479
|
+
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
29480
|
+
*/
|
|
29481
|
+
function runFrameProcessorPass(input) {
|
|
29482
|
+
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
29483
|
+
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
29484
|
+
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
29485
|
+
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
29486
|
+
const accum = /* @__PURE__ */ new Map();
|
|
29487
|
+
for (const { timestamp, frame } of input.frames) {
|
|
29488
|
+
const result = processor.process({
|
|
29489
|
+
timestamp,
|
|
29490
|
+
frame
|
|
29491
|
+
});
|
|
29492
|
+
for (const td of result.tracked) {
|
|
29493
|
+
const existing = accum.get(td.trackId);
|
|
29494
|
+
const matched = td.matchedThisFrame ? 1 : 0;
|
|
29495
|
+
if (existing === void 0) accum.set(td.trackId, {
|
|
29496
|
+
className: td.className,
|
|
29497
|
+
firstSeenMs: timestamp,
|
|
29498
|
+
lastSeenMs: timestamp,
|
|
29499
|
+
bbox: { ...td.bbox },
|
|
29500
|
+
framesMatched: matched
|
|
29501
|
+
});
|
|
29502
|
+
else {
|
|
29503
|
+
existing.className = td.className;
|
|
29504
|
+
existing.lastSeenMs = timestamp;
|
|
29505
|
+
existing.framesMatched += matched;
|
|
29506
|
+
}
|
|
29507
|
+
}
|
|
29508
|
+
}
|
|
29509
|
+
return { tracks: [...accum.values()].map((t) => ({
|
|
29510
|
+
className: t.className,
|
|
29511
|
+
firstSeenMs: t.firstSeenMs,
|
|
29512
|
+
lastSeenMs: t.lastSeenMs,
|
|
29513
|
+
bbox: t.bbox,
|
|
29514
|
+
framesMatched: t.framesMatched
|
|
29515
|
+
})) };
|
|
29516
|
+
}
|
|
29517
|
+
//#endregion
|
|
29410
29518
|
//#region src/pipeline-analytics/pipeline/semantic-detail-bbox.ts
|
|
29411
29519
|
/** Which box the semantic best-shot should crop: a plate/face DETAIL box
|
|
29412
29520
|
* when we have one, otherwise the subject box. */
|
|
@@ -31823,11 +31931,13 @@ var MediaStore = class {
|
|
|
31823
31931
|
store;
|
|
31824
31932
|
logger;
|
|
31825
31933
|
writeGate;
|
|
31934
|
+
resolveEventMediaLocation;
|
|
31826
31935
|
constructor(deps) {
|
|
31827
31936
|
this.storage = deps.storage;
|
|
31828
31937
|
this.store = deps.store;
|
|
31829
31938
|
this.logger = deps.logger;
|
|
31830
31939
|
this.writeGate = deps.writeGate;
|
|
31940
|
+
this.resolveEventMediaLocation = deps.resolveEventMediaLocation;
|
|
31831
31941
|
}
|
|
31832
31942
|
gated(mutation) {
|
|
31833
31943
|
return this.writeGate ? this.writeGate.run(mutation) : mutation();
|
|
@@ -31848,7 +31958,8 @@ var MediaStore = class {
|
|
|
31848
31958
|
async putUnchecked(params) {
|
|
31849
31959
|
const key = buildKey(params);
|
|
31850
31960
|
const path = buildPath(params);
|
|
31851
|
-
const
|
|
31961
|
+
const staticLocation = locationForOwnerKind(params.ownerKind);
|
|
31962
|
+
const location = staticLocation === "eventMedia" && this.resolveEventMediaLocation ? await this.resolveEventMediaLocation(params.deviceId) : staticLocation;
|
|
31852
31963
|
const record = {
|
|
31853
31964
|
deviceId: params.deviceId,
|
|
31854
31965
|
ownerKind: params.ownerKind,
|
|
@@ -41541,6 +41652,21 @@ var MediaCaptureLogAggregator = class {
|
|
|
41541
41652
|
}
|
|
41542
41653
|
};
|
|
41543
41654
|
//#endregion
|
|
41655
|
+
//#region src/pipeline-analytics/media-locations.ts
|
|
41656
|
+
/**
|
|
41657
|
+
* Picks the location a new `eventMedia` blob should be written to, or `null`
|
|
41658
|
+
* when none is writable — the caller MUST refuse the write rather than fall
|
|
41659
|
+
* back to a read-only location, which would fail far downstream as an opaque
|
|
41660
|
+
* filesystem error instead of a named storage decision.
|
|
41661
|
+
*/
|
|
41662
|
+
function pickWritableMediaLocation(locations, currentDefaultId) {
|
|
41663
|
+
const writable = locations.filter((l) => l.enabled && !l.readOnly);
|
|
41664
|
+
if (writable.length === 0) return null;
|
|
41665
|
+
const keep = writable.find((l) => l.id === currentDefaultId);
|
|
41666
|
+
if (keep) return keep.id;
|
|
41667
|
+
return writable.toSorted((a, b) => b.headroomBytes - a.headroomBytes)[0]?.id ?? null;
|
|
41668
|
+
}
|
|
41669
|
+
//#endregion
|
|
41544
41670
|
//#region src/pipeline-analytics/media-relocate-engine.ts
|
|
41545
41671
|
var PAGE_SIZE = 200;
|
|
41546
41672
|
var DEFAULT_THROTTLE_MBPS = 40;
|
|
@@ -47611,6 +47737,37 @@ var FaceStore = class {
|
|
|
47611
47737
|
}
|
|
47612
47738
|
}
|
|
47613
47739
|
/**
|
|
47740
|
+
* Persist a `face.rescoreTrack` result onto the SUGGESTED tier — the ONE
|
|
47741
|
+
* write that explicitly-requested action makes. `suggestion: null` means
|
|
47742
|
+
* the recompute found no candidate worth offering and CLEARS both columns
|
|
47743
|
+
* (a genuine "the gallery has nothing for this face right now" result, not
|
|
47744
|
+
* a no-op left at whatever they held before). Never touches `assigned` /
|
|
47745
|
+
* `recognizedIdentityId` — a suggestion is never an assignment. The caller
|
|
47746
|
+
* is responsible for never calling this with a non-comparable (`model-
|
|
47747
|
+
* mismatch`) report; this method persists whatever it is given.
|
|
47748
|
+
*/
|
|
47749
|
+
async setSuggestion(faceId, suggestion) {
|
|
47750
|
+
const data = suggestion === null ? {
|
|
47751
|
+
suggestedIdentityId: null,
|
|
47752
|
+
suggestedMatchScore: null
|
|
47753
|
+
} : {
|
|
47754
|
+
suggestedIdentityId: suggestion.identityId,
|
|
47755
|
+
suggestedMatchScore: suggestion.score
|
|
47756
|
+
};
|
|
47757
|
+
try {
|
|
47758
|
+
await this.store.update.mutate({
|
|
47759
|
+
collection: FACES_COLLECTION,
|
|
47760
|
+
id: faceId,
|
|
47761
|
+
data
|
|
47762
|
+
});
|
|
47763
|
+
} catch (err) {
|
|
47764
|
+
this.logger.warn("FaceStore.setSuggestion failed", { meta: {
|
|
47765
|
+
faceId,
|
|
47766
|
+
error: String(err)
|
|
47767
|
+
} });
|
|
47768
|
+
}
|
|
47769
|
+
}
|
|
47770
|
+
/**
|
|
47614
47771
|
* `TrackScopedStore` contract for the retention cascade: delete the buffered
|
|
47615
47772
|
* face rows of the given tracks that are NOT assigned. Reuses the EXACT
|
|
47616
47773
|
* `assigned` exemption that `prune`/`pruneAll` apply (see the
|
|
@@ -57034,6 +57191,144 @@ function rowFromMetadata(id, metadata) {
|
|
|
57034
57191
|
};
|
|
57035
57192
|
}
|
|
57036
57193
|
//#endregion
|
|
57194
|
+
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
57195
|
+
/** How long a mirrored entry is served before a refresh is started behind the
|
|
57196
|
+
* next reader. See the module doc for why this matches the per-kind caches'
|
|
57197
|
+
* own window rather than introducing a second staleness bound. */
|
|
57198
|
+
var RAW_SETTINGS_STALE_MS = 5e3;
|
|
57199
|
+
/**
|
|
57200
|
+
* How long a FAILED refresh is not retried.
|
|
57201
|
+
*
|
|
57202
|
+
* A refresh that throws leaves `fetchedAt` where it was, so without a
|
|
57203
|
+
* cooldown every subsequent reader would start another attempt — a broken
|
|
57204
|
+
* settings store would be hammered at frame rate by the very mechanism built
|
|
57205
|
+
* to stop that.
|
|
57206
|
+
*/
|
|
57207
|
+
var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
|
|
57208
|
+
/**
|
|
57209
|
+
* The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
|
|
57210
|
+
* fresh OFF every per-kind settings cache's hot path.
|
|
57211
|
+
*
|
|
57212
|
+
* `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
|
|
57213
|
+
* (or frames) missing on the same device in the same tick costs ONE read,
|
|
57214
|
+
* not one per caller — the exact failure this class exists to remove.
|
|
57215
|
+
*/
|
|
57216
|
+
var RawDeviceBlobMirror = class {
|
|
57217
|
+
deps;
|
|
57218
|
+
staleMs;
|
|
57219
|
+
missCooldownMs;
|
|
57220
|
+
entries = /* @__PURE__ */ new Map();
|
|
57221
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
57222
|
+
now;
|
|
57223
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
57224
|
+
this.deps = deps;
|
|
57225
|
+
this.staleMs = staleMs;
|
|
57226
|
+
this.missCooldownMs = missCooldownMs;
|
|
57227
|
+
this.now = deps.now ?? (() => Date.now());
|
|
57228
|
+
}
|
|
57229
|
+
/**
|
|
57230
|
+
* The device's raw stored blob.
|
|
57231
|
+
*
|
|
57232
|
+
* COLD awaits — the first settings kind to touch a device has to get the
|
|
57233
|
+
* real answer. WARM returns immediately, stale or not, and starts the
|
|
57234
|
+
* refresh behind the caller.
|
|
57235
|
+
*/
|
|
57236
|
+
async resolve(deviceId) {
|
|
57237
|
+
const entry = this.entries.get(deviceId);
|
|
57238
|
+
if (entry === void 0) return this.load(deviceId);
|
|
57239
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
|
|
57240
|
+
return entry.value;
|
|
57241
|
+
}
|
|
57242
|
+
/** Drop a device's entry. Called by every in-process writer of this
|
|
57243
|
+
* device's row, so an operator's own change is never served stale. */
|
|
57244
|
+
invalidate(deviceId) {
|
|
57245
|
+
this.entries.delete(deviceId);
|
|
57246
|
+
}
|
|
57247
|
+
invalidateAll() {
|
|
57248
|
+
this.entries.clear();
|
|
57249
|
+
}
|
|
57250
|
+
async load(deviceId) {
|
|
57251
|
+
const pending = this.inFlight.get(deviceId);
|
|
57252
|
+
if (pending !== void 0) return pending;
|
|
57253
|
+
const run = this.deps.read(deviceId).then((value) => {
|
|
57254
|
+
this.entries.set(deviceId, {
|
|
57255
|
+
value,
|
|
57256
|
+
fetchedAt: this.now()
|
|
57257
|
+
});
|
|
57258
|
+
return value;
|
|
57259
|
+
}).catch((err) => {
|
|
57260
|
+
this.deps.onRefreshFailed?.(deviceId, err);
|
|
57261
|
+
const existing = this.entries.get(deviceId);
|
|
57262
|
+
if (existing !== void 0) {
|
|
57263
|
+
existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
|
|
57264
|
+
return existing.value;
|
|
57265
|
+
}
|
|
57266
|
+
return {};
|
|
57267
|
+
}).finally(() => {
|
|
57268
|
+
this.inFlight.delete(deviceId);
|
|
57269
|
+
});
|
|
57270
|
+
this.inFlight.set(deviceId, run);
|
|
57271
|
+
return run;
|
|
57272
|
+
}
|
|
57273
|
+
};
|
|
57274
|
+
/**
|
|
57275
|
+
* The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
|
|
57276
|
+
* kept fresh OFF every per-kind settings cache's hot path. Same shape as
|
|
57277
|
+
* {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
|
|
57278
|
+
* one addon store per process.
|
|
57279
|
+
*/
|
|
57280
|
+
var RawGlobalStoreMirror = class {
|
|
57281
|
+
deps;
|
|
57282
|
+
staleMs;
|
|
57283
|
+
missCooldownMs;
|
|
57284
|
+
entry = null;
|
|
57285
|
+
inFlight = null;
|
|
57286
|
+
now;
|
|
57287
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
57288
|
+
this.deps = deps;
|
|
57289
|
+
this.staleMs = staleMs;
|
|
57290
|
+
this.missCooldownMs = missCooldownMs;
|
|
57291
|
+
this.now = deps.now ?? (() => Date.now());
|
|
57292
|
+
}
|
|
57293
|
+
async resolve() {
|
|
57294
|
+
const entry = this.entry;
|
|
57295
|
+
if (entry === null) return this.load();
|
|
57296
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
|
|
57297
|
+
return entry.value;
|
|
57298
|
+
}
|
|
57299
|
+
/** Drop the mirrored store. Called by every in-process global-settings
|
|
57300
|
+
* writer (`onConfigChanged`), so an operator's own change is never served
|
|
57301
|
+
* stale. */
|
|
57302
|
+
invalidate() {
|
|
57303
|
+
this.entry = null;
|
|
57304
|
+
}
|
|
57305
|
+
async load() {
|
|
57306
|
+
if (this.inFlight !== null) return this.inFlight;
|
|
57307
|
+
const run = this.deps.read().then((value) => {
|
|
57308
|
+
this.entry = {
|
|
57309
|
+
value,
|
|
57310
|
+
fetchedAt: this.now()
|
|
57311
|
+
};
|
|
57312
|
+
return value;
|
|
57313
|
+
}).catch((err) => {
|
|
57314
|
+
this.deps.onRefreshFailed?.(err);
|
|
57315
|
+
const existing = this.entry;
|
|
57316
|
+
if (existing !== null) {
|
|
57317
|
+
this.entry = {
|
|
57318
|
+
value: existing.value,
|
|
57319
|
+
fetchedAt: this.now() - this.staleMs + this.missCooldownMs
|
|
57320
|
+
};
|
|
57321
|
+
return existing.value;
|
|
57322
|
+
}
|
|
57323
|
+
return {};
|
|
57324
|
+
}).finally(() => {
|
|
57325
|
+
this.inFlight = null;
|
|
57326
|
+
});
|
|
57327
|
+
this.inFlight = run;
|
|
57328
|
+
return run;
|
|
57329
|
+
}
|
|
57330
|
+
};
|
|
57331
|
+
//#endregion
|
|
57037
57332
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
57038
57333
|
/**
|
|
57039
57334
|
* Sweep every device with persisted tracks: skip retention-disabled devices,
|
|
@@ -58090,6 +58385,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58090
58385
|
/** Event-media relocation engine (entity-routing Phase 4). */
|
|
58091
58386
|
mediaRelocate = null;
|
|
58092
58387
|
mediaLocationStorage = null;
|
|
58388
|
+
/**
|
|
58389
|
+
* Cached pick for NEW `eventMedia` writes (`pickWritableMediaLocation`).
|
|
58390
|
+
* Resolved once, never per blob (no-RPC-per-blob, same discipline as
|
|
58391
|
+
* `mediaLocationStorage`'s own root cache) — dropped together with it by
|
|
58392
|
+
* `refreshStorageLocationsForMigration`, since a location's `readOnly` /
|
|
58393
|
+
* `enabled` flags only change via operator reconfig.
|
|
58394
|
+
*/
|
|
58395
|
+
eventMediaWriteLocationCache = null;
|
|
58093
58396
|
mediaWriteGate = new MediaWriteGate();
|
|
58094
58397
|
storageMigrationLeaseId = null;
|
|
58095
58398
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
@@ -58489,6 +58792,33 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58489
58792
|
audioEpisodeTracks = /* @__PURE__ */ new Map();
|
|
58490
58793
|
audioEpisodeLastPersistMs = /* @__PURE__ */ new Map();
|
|
58491
58794
|
settingsCaches = new DeviceSettingsCacheHub();
|
|
58795
|
+
/**
|
|
58796
|
+
* The addon-level store and per-device blob, mirrored ONCE and shared by
|
|
58797
|
+
* every settings kind above — see `store/settings-raw-mirror.ts`. Before
|
|
58798
|
+
* this, each kind's cache miss called `resolveGlobalStore()` +
|
|
58799
|
+
* `readDeviceBlob()` directly, so eight kinds expiring in the same window
|
|
58800
|
+
* paid for the same two reads eight times: `get
|
|
58801
|
+
* pipeline-analytics:addon-settings` at 547–694/min, `get
|
|
58802
|
+
* pipeline-analytics:addon-devices` at ~700/min, live 2026-08-26. Invalidated
|
|
58803
|
+
* by every in-process writer: `applyDeviceSettingsPatch` and the retired-
|
|
58804
|
+
* setting purges drop the device mirror, `onConfigChanged` (this process's
|
|
58805
|
+
* own `updateGlobalSettings`) drops the global mirror.
|
|
58806
|
+
*/
|
|
58807
|
+
rawDeviceBlobMirror = new RawDeviceBlobMirror({
|
|
58808
|
+
read: (deviceId) => this.readDeviceBlob(deviceId),
|
|
58809
|
+
onRefreshFailed: (deviceId, err) => {
|
|
58810
|
+
this.ctx.logger.debug("raw device-blob mirror refresh failed — serving last value", {
|
|
58811
|
+
tags: { deviceId },
|
|
58812
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
58813
|
+
});
|
|
58814
|
+
}
|
|
58815
|
+
});
|
|
58816
|
+
rawGlobalStoreMirror = new RawGlobalStoreMirror({
|
|
58817
|
+
read: () => this.resolveGlobalStore(),
|
|
58818
|
+
onRefreshFailed: (err) => {
|
|
58819
|
+
this.ctx.logger.debug("raw global-store mirror refresh failed — serving last value", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
58820
|
+
}
|
|
58821
|
+
});
|
|
58492
58822
|
/** Avoids a store read every ~30 Hz audio frame. */
|
|
58493
58823
|
audioSettingsCache = this.settingsCaches.registerDeviceCache("audio-detection", SETTINGS_CACHE_TTL_MS);
|
|
58494
58824
|
sensitivitySettingsCache = this.settingsCaches.registerDeviceCache("detection-sensitivity", SETTINGS_CACHE_TTL_MS);
|
|
@@ -59208,6 +59538,53 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
59208
59538
|
this.mediaLocationStorage = routed;
|
|
59209
59539
|
return routed;
|
|
59210
59540
|
}
|
|
59541
|
+
/**
|
|
59542
|
+
* Resolves which `eventMedia` location a NEW footage-class blob should be
|
|
59543
|
+
* written to (`MediaStoreDeps.resolveEventMediaLocation`). Reflects
|
|
59544
|
+
* `config.readOnly` — an operator draining a location marks it read-only,
|
|
59545
|
+
* and new writes must never land there (the relocate mover, exposed as
|
|
59546
|
+
* `relocateMedia`, empties what is already on it in the background).
|
|
59547
|
+
*
|
|
59548
|
+
* Cached after the first successful resolution — this is a per-blob write
|
|
59549
|
+
* path, and the write-side provider is a declared no-RPC-per-blob bypass
|
|
59550
|
+
* (D-note in `location-aware-media-storage.ts`); re-querying the storage
|
|
59551
|
+
* cap on every blob would undo that. Dropped by
|
|
59552
|
+
* `refreshStorageLocationsForMigration`, same trigger as
|
|
59553
|
+
* `mediaLocationStorage.invalidateRoots()`.
|
|
59554
|
+
*
|
|
59555
|
+
* Degrades to the bare `eventMedia` type ref (pre-2026-08-26 behaviour)
|
|
59556
|
+
* when the storage cap itself is unreachable — a transient RPC failure (or
|
|
59557
|
+
* a test harness that never wires `api.storage`) must not turn every media
|
|
59558
|
+
* write into a hard failure the way "no writable location" does.
|
|
59559
|
+
*/
|
|
59560
|
+
async resolveEventMediaWriteLocation(api, logger, deviceId) {
|
|
59561
|
+
const cached = this.eventMediaWriteLocationCache;
|
|
59562
|
+
if (cached !== null) return cached;
|
|
59563
|
+
let locations;
|
|
59564
|
+
try {
|
|
59565
|
+
locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
|
|
59566
|
+
} catch (err) {
|
|
59567
|
+
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: require_dist.errMsg(err) } });
|
|
59568
|
+
this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
|
|
59569
|
+
return DEFAULT_MEDIA_LOCATION;
|
|
59570
|
+
}
|
|
59571
|
+
const picked = pickWritableMediaLocation(locations.map((l) => ({
|
|
59572
|
+
id: l.id,
|
|
59573
|
+
readOnly: l.config["readOnly"] === true,
|
|
59574
|
+
enabled: l.enabled !== false,
|
|
59575
|
+
headroomBytes: l.capacity?.availableBytes ?? 0
|
|
59576
|
+
})), locations.find((l) => l.isDefault)?.id ?? "eventMedia");
|
|
59577
|
+
if (picked === null) {
|
|
59578
|
+
const seen = locations.length > 0 ? locations.map((l) => l.id).join(", ") : "(none registered)";
|
|
59579
|
+
logger.error("pipeline-analytics: no writable eventMedia storage location — refusing the write instead of failing far downstream as a filesystem error", {
|
|
59580
|
+
tags: { deviceId },
|
|
59581
|
+
meta: { locationsSeen: seen }
|
|
59582
|
+
});
|
|
59583
|
+
throw new Error(`no writable eventMedia storage location (seen: ${seen})`);
|
|
59584
|
+
}
|
|
59585
|
+
this.eventMediaWriteLocationCache = picked;
|
|
59586
|
+
return picked;
|
|
59587
|
+
}
|
|
59211
59588
|
/** Constructs every SQLite-backed store plus the stationary/package-drop/
|
|
59212
59589
|
* sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
|
|
59213
59590
|
* so the rest of boot consumes the stores without re-narrowing. */
|
|
@@ -59266,7 +59643,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
59266
59643
|
storage,
|
|
59267
59644
|
store: api.settingsStore,
|
|
59268
59645
|
logger: logger.child("MediaStore"),
|
|
59269
|
-
writeGate: this.mediaWriteGate
|
|
59646
|
+
writeGate: this.mediaWriteGate,
|
|
59647
|
+
resolveEventMediaLocation: (deviceId) => this.resolveEventMediaWriteLocation(api, logger, deviceId)
|
|
59270
59648
|
});
|
|
59271
59649
|
this.mediaStore = mediaStore;
|
|
59272
59650
|
const retrainFrames = new RetrainFrameStore({
|
|
@@ -60299,6 +60677,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60299
60677
|
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
|
|
60300
60678
|
});
|
|
60301
60679
|
}
|
|
60680
|
+
/**
|
|
60681
|
+
* The FrameProcessor pass of a replay run — see the cap doc on
|
|
60682
|
+
* `runReplayFrameProcessor` for why this is not the `processFrame` method
|
|
60683
|
+
* this addon deliberately does not have.
|
|
60684
|
+
*
|
|
60685
|
+
* `runFrameProcessorPass` is pure w.r.t. this addon's own state: it
|
|
60686
|
+
* constructs a FRESH `FrameProcessor` and does not touch `this.trackStore`,
|
|
60687
|
+
* `this.stationaryRegistry` or any other instance field — unlike
|
|
60688
|
+
* `getOrCreateProcessor` (the production path), which wires both.
|
|
60689
|
+
* `scripts/check-replay-isolation.ts` holds that boundary on the
|
|
60690
|
+
* `replay/` directory this delegates into.
|
|
60691
|
+
*/
|
|
60692
|
+
async runReplayFrameProcessor(input) {
|
|
60693
|
+
return runFrameProcessorPass(input);
|
|
60694
|
+
}
|
|
60302
60695
|
async listRetrainAnnotations(input) {
|
|
60303
60696
|
return this.requireRetrain().listAnnotations(input.trackId);
|
|
60304
60697
|
}
|
|
@@ -60574,6 +60967,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60574
60967
|
this.forgetDeviceProcessors(data.deviceId);
|
|
60575
60968
|
this.levelStateByDevice.delete(data.deviceId);
|
|
60576
60969
|
this.settingsCaches.clearDevice(data.deviceId);
|
|
60970
|
+
this.rawDeviceBlobMirror.invalidate(data.deviceId);
|
|
60577
60971
|
}
|
|
60578
60972
|
}
|
|
60579
60973
|
/** `DeviceUnregistered` handler — subscription topology lives in
|
|
@@ -60598,6 +60992,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60598
60992
|
}
|
|
60599
60993
|
this.audioMarkers?.forgetDevice(deviceId);
|
|
60600
60994
|
this.settingsCaches.clearDevice(deviceId);
|
|
60995
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
60601
60996
|
this.bindingCache?.invalidate(deviceId);
|
|
60602
60997
|
this.detectionCoverage?.forget(deviceId);
|
|
60603
60998
|
this.zoneAnalytics?.forgetDevice(deviceId);
|
|
@@ -61026,6 +61421,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61026
61421
|
}] : []
|
|
61027
61422
|
];
|
|
61028
61423
|
}
|
|
61424
|
+
/**
|
|
61425
|
+
* `BaseAddon.updateGlobalSettings` calls this after the write lands —
|
|
61426
|
+
* gated to the LOCAL node's own write (a sibling-node write returns before
|
|
61427
|
+
* reaching it), which is exactly the scope `rawGlobalStoreMirror` needs to
|
|
61428
|
+
* invalidate: the mirror only ever answers for THIS process, so it only
|
|
61429
|
+
* needs to drop on THIS process's own write (D224).
|
|
61430
|
+
*/
|
|
61431
|
+
async onConfigChanged() {
|
|
61432
|
+
await super.onConfigChanged();
|
|
61433
|
+
this.rawGlobalStoreMirror.invalidate();
|
|
61434
|
+
}
|
|
61029
61435
|
async onShutdown() {
|
|
61030
61436
|
this.shuttingDown = true;
|
|
61031
61437
|
this.sceneEngine?.stop();
|
|
@@ -61081,6 +61487,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61081
61487
|
this.gatedNodeByDevice.clear();
|
|
61082
61488
|
this.levelStateByDevice.clear();
|
|
61083
61489
|
this.settingsCaches.clearAll();
|
|
61490
|
+
this.rawDeviceBlobMirror.invalidateAll();
|
|
61491
|
+
this.rawGlobalStoreMirror.invalidate();
|
|
61084
61492
|
this.confirmationDetectorStepByNode.clear();
|
|
61085
61493
|
this.trackStore?.clearAll();
|
|
61086
61494
|
this.stationaryRegistry = null;
|
|
@@ -61793,10 +62201,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61793
62201
|
* auto-assigning — see `__tests__/global-settings-cascade.spec.ts`).
|
|
61794
62202
|
* `resolveGlobalStore()` is the sanctioned BaseAddon read (per-node
|
|
61795
62203
|
* projection applied; a raw `readAddonStore` would leak `@node` keys if a
|
|
61796
|
-
* perNode field is ever declared).
|
|
62204
|
+
* perNode field is ever declared).
|
|
62205
|
+
*
|
|
62206
|
+
* Both raw reads go through `rawGlobalStoreMirror` / `rawDeviceBlobMirror`
|
|
62207
|
+
* rather than straight to the store: this method is the ONE place all
|
|
62208
|
+
* eight settings kinds funnel through on a cache miss, so calling the raw
|
|
62209
|
+
* reads directly here means eight kinds expiring in the same window each
|
|
62210
|
+
* pay for the same two reads — see `store/settings-raw-mirror.ts`. */
|
|
61797
62211
|
async readDeviceSettings(deviceId, resolve) {
|
|
61798
|
-
const globalRaw = this.ctxIfReady !== null ? await this.
|
|
61799
|
-
const deviceRaw = await this.
|
|
62212
|
+
const globalRaw = this.ctxIfReady !== null ? await this.rawGlobalStoreMirror.resolve() : {};
|
|
62213
|
+
const deviceRaw = await this.rawDeviceBlobMirror.resolve(deviceId);
|
|
61800
62214
|
return resolve({
|
|
61801
62215
|
...globalRaw,
|
|
61802
62216
|
...deviceRaw
|
|
@@ -63467,6 +63881,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63467
63881
|
});
|
|
63468
63882
|
}
|
|
63469
63883
|
await settings.writeDeviceStore(deviceId, plan.patch);
|
|
63884
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
63470
63885
|
} catch (err) {
|
|
63471
63886
|
this.retiredSettingsChecked.delete(deviceId);
|
|
63472
63887
|
this.ctx.logger.warn("retired retention settings migration failed — will retry", {
|
|
@@ -63538,6 +63953,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63538
63953
|
deviceId
|
|
63539
63954
|
});
|
|
63540
63955
|
await settings.writeDeviceStore(deviceId, plan.patch);
|
|
63956
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
63541
63957
|
} catch (err) {
|
|
63542
63958
|
this.storedSuggestionCheckedDevices.delete(deviceId);
|
|
63543
63959
|
this.ctx.logger.warn("face suggestion threshold purge failed — will retry", {
|
|
@@ -65225,6 +65641,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65225
65641
|
* See `face-rescore.ts` for why the number already on the track cannot answer
|
|
65226
65642
|
* that: it was computed during live detection and persisted at track close,
|
|
65227
65643
|
* so every enrolment made since is invisible to it.
|
|
65644
|
+
*
|
|
65645
|
+
* The report's result is PERSISTED (`persistRescoreResult`, below) onto
|
|
65646
|
+
* `Face.suggestedIdentityId` / `suggestedMatchScore` — the SAME columns the
|
|
65647
|
+
* live recognizer's suggestion band writes, so a reader never has to know
|
|
65648
|
+
* whether a suggestion came from track close or from this button. Still
|
|
65649
|
+
* never a background job: this write happens once, only because the
|
|
65650
|
+
* operator asked THIS question right now.
|
|
65228
65651
|
*/
|
|
65229
65652
|
async rescoreFaceForTrack(input) {
|
|
65230
65653
|
const api = this.ctx.api;
|
|
@@ -65267,6 +65690,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65267
65690
|
clusterModelId,
|
|
65268
65691
|
params
|
|
65269
65692
|
});
|
|
65693
|
+
const persisted = await persistRescoreResult(faceId, report, { setSuggestion: (id, suggestion) => faceStore.setSuggestion(id, suggestion) });
|
|
65270
65694
|
this.ctx.logger.info("face rescore", {
|
|
65271
65695
|
tags: { deviceId: input.deviceId },
|
|
65272
65696
|
meta: {
|
|
@@ -65280,7 +65704,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65280
65704
|
best: report.identities[0]?.name ?? null,
|
|
65281
65705
|
bestScore: report.identities[0]?.score ?? null,
|
|
65282
65706
|
storedSuggestion: face.suggestedMatchScore ?? null,
|
|
65283
|
-
reembedded: reembed.succeeded
|
|
65707
|
+
reembedded: reembed.succeeded,
|
|
65708
|
+
persisted
|
|
65284
65709
|
}
|
|
65285
65710
|
});
|
|
65286
65711
|
return {
|
|
@@ -65684,6 +66109,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65684
66109
|
async refreshStorageLocationsForMigration(input) {
|
|
65685
66110
|
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
|
|
65686
66111
|
this.mediaLocationStorage?.invalidateRoots();
|
|
66112
|
+
this.eventMediaWriteLocationCache = null;
|
|
65687
66113
|
return { refreshed: true };
|
|
65688
66114
|
}
|
|
65689
66115
|
async startStorageMigrationMove(input) {
|
|
@@ -65697,6 +66123,32 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65697
66123
|
async cancelStorageMigrationMove(input) {
|
|
65698
66124
|
return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
|
|
65699
66125
|
}
|
|
66126
|
+
/**
|
|
66127
|
+
* Move event media between locations — the OPERATOR's mover, no lease.
|
|
66128
|
+
* Twin of `recording.relocateFootage`: same engine (`this.mediaRelocate`)
|
|
66129
|
+
* the lease-gated coordinator move uses, runnable in the background
|
|
66130
|
+
* without pausing anything. `startStorageMigrationMove` above stays
|
|
66131
|
+
* lease-gated for the orchestrated migration; this method must never touch
|
|
66132
|
+
* `storageMigrationLeaseId`.
|
|
66133
|
+
*/
|
|
66134
|
+
async relocateMedia(input) {
|
|
66135
|
+
if (!this.mediaRelocate) throw new Error("media relocation unavailable");
|
|
66136
|
+
const jobId = this.mediaRelocate.start(input);
|
|
66137
|
+
this.ctx.logger.info("pipeline-analytics: media relocate job enqueued", { meta: {
|
|
66138
|
+
jobId,
|
|
66139
|
+
to: input.toLocationId
|
|
66140
|
+
} });
|
|
66141
|
+
return { jobId };
|
|
66142
|
+
}
|
|
66143
|
+
/** Every relocate job this addon knows about, newest first (in RAM: the
|
|
66144
|
+
* move is resumable, so a lost list costs nothing but the display). */
|
|
66145
|
+
async listRelocateMediaJobs() {
|
|
66146
|
+
return this.mediaRelocate?.list() ?? [];
|
|
66147
|
+
}
|
|
66148
|
+
/** Cancel a running or queued relocate job. */
|
|
66149
|
+
async cancelRelocateMedia(input) {
|
|
66150
|
+
return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
|
|
66151
|
+
}
|
|
65700
66152
|
async listOpsLog(input) {
|
|
65701
66153
|
return this.queryFacade.listOpsLog(input);
|
|
65702
66154
|
}
|
|
@@ -66090,6 +66542,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
66090
66542
|
}
|
|
66091
66543
|
async applyDeviceSettingsPatch(input) {
|
|
66092
66544
|
await this.updateDeviceSettings(input.deviceId, input.patch);
|
|
66545
|
+
this.rawDeviceBlobMirror.invalidate(input.deviceId);
|
|
66093
66546
|
this.audioSettingsCache.delete(input.deviceId);
|
|
66094
66547
|
this.sensitivitySettingsCache.delete(input.deviceId);
|
|
66095
66548
|
this.trackingSettingsCache.delete(input.deviceId);
|