@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-
|
|
1
|
+
import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-CGhP6T_c.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -27195,6 +27195,18 @@ var FaceGalleryProvider = class {
|
|
|
27195
27195
|
* This module names that state (`model-mismatch`) so the UI can say "this face
|
|
27196
27196
|
* is in an old feature space" instead of showing 0% and letting the operator
|
|
27197
27197
|
* conclude the person is not there.
|
|
27198
|
+
*
|
|
27199
|
+
* ## `buildFaceRescoreReport` stays pure — the CALLER persists
|
|
27200
|
+
*
|
|
27201
|
+
* This function never touches the store; every input is passed in and nothing
|
|
27202
|
+
* is read or written (see its own doc below). The operator asked for this
|
|
27203
|
+
* exact recompute, though, and wants the answer to stick — so the ACTION that
|
|
27204
|
+
* wraps this report (`rescoreFaceForTrack` in `pipeline-analytics/index.ts`)
|
|
27205
|
+
* writes the result onto `Face.suggestedIdentityId` / `suggestedMatchScore`,
|
|
27206
|
+
* through `decideRescorePersistence` / `persistRescoreResult` below. That is
|
|
27207
|
+
* still not a background job — it is the SAME explicitly-requested call,
|
|
27208
|
+
* writing once. A `model-mismatch` report (`comparable: false`) is never
|
|
27209
|
+
* written: see `persistRescoreResult`.
|
|
27198
27210
|
*/
|
|
27199
27211
|
/** The empty report every "cannot score" path returns, so shape never varies. */
|
|
27200
27212
|
function emptyReport(verdict, input, comparable) {
|
|
@@ -27270,6 +27282,52 @@ function buildFaceRescoreReport(input) {
|
|
|
27270
27282
|
...best !== void 0 && second !== void 0 ? { runnerUpGap: best.score - second.score } : {}
|
|
27271
27283
|
};
|
|
27272
27284
|
}
|
|
27285
|
+
/**
|
|
27286
|
+
* What `face.rescoreTrack` should PERSIST for a COMPARABLE report, decided as
|
|
27287
|
+
* its own pure function so the rule is directly testable — separated from the
|
|
27288
|
+
* class that owns the store I/O (`rescoreFaceForTrack` in
|
|
27289
|
+
* `pipeline-analytics/index.ts`), which only wires this answer to
|
|
27290
|
+
* `FaceStore.setSuggestion`.
|
|
27291
|
+
*
|
|
27292
|
+
* `null` covers every verdict OTHER than `auto-assignable` / `suggestion`, and
|
|
27293
|
+
* also an `auto-assignable` / `suggestion` verdict whose best-ranked row is
|
|
27294
|
+
* not itself ELIGIBLE (`identities` is ranked by raw score across every
|
|
27295
|
+
* comparable identity, eligible or not — see `buildFaceRescoreReport`). Every
|
|
27296
|
+
* one of those is a genuine "nothing to suggest right now": never an
|
|
27297
|
+
* assignment, and never the winner of an `ambiguous` verdict — the matcher
|
|
27298
|
+
* refused to pick one, so this function does not pick one either.
|
|
27299
|
+
*
|
|
27300
|
+
* Callers MUST gate on `report.comparable` before calling this — a
|
|
27301
|
+
* `model-mismatch` report has no verdict worth acting on; see
|
|
27302
|
+
* {@link persistRescoreResult}, which does that gating and is the one thing
|
|
27303
|
+
* `rescoreFaceForTrack` actually calls.
|
|
27304
|
+
*/
|
|
27305
|
+
function decideRescorePersistence(report) {
|
|
27306
|
+
if (report.verdict !== "auto-assignable" && report.verdict !== "suggestion") return null;
|
|
27307
|
+
const candidate = report.identities.find((row) => row.eligible);
|
|
27308
|
+
return candidate === void 0 ? null : {
|
|
27309
|
+
identityId: candidate.identityId,
|
|
27310
|
+
score: candidate.score
|
|
27311
|
+
};
|
|
27312
|
+
}
|
|
27313
|
+
/**
|
|
27314
|
+
* Persist `face.rescoreTrack`'s result onto the SUGGESTED tier — the ONE
|
|
27315
|
+
* write this explicitly-requested action makes, and the only place that
|
|
27316
|
+
* decides whether it happens at all.
|
|
27317
|
+
*
|
|
27318
|
+
* A non-comparable report (`model-mismatch`) short-circuits BEFORE
|
|
27319
|
+
* `ports.setSuggestion` is ever called: a cosine across model spaces is
|
|
27320
|
+
* well-formed noise, and persisting it would look exactly like a fresh,
|
|
27321
|
+
* meaningful read to every future reader of the row. This is the single most
|
|
27322
|
+
* load-bearing branch in this file — the regression it guards against writes
|
|
27323
|
+
* garbage that reads as data.
|
|
27324
|
+
*/
|
|
27325
|
+
async function persistRescoreResult(faceId, report, ports) {
|
|
27326
|
+
if (!report.comparable) return "not-comparable";
|
|
27327
|
+
const candidate = decideRescorePersistence(report);
|
|
27328
|
+
await ports.setSuggestion(faceId, candidate);
|
|
27329
|
+
return candidate !== null ? "written" : "cleared";
|
|
27330
|
+
}
|
|
27273
27331
|
//#endregion
|
|
27274
27332
|
//#region src/pipeline-analytics/face-rescore-actions.ts
|
|
27275
27333
|
/**
|
|
@@ -27355,9 +27413,12 @@ var FaceReembedReportSchema = object({
|
|
|
27355
27413
|
});
|
|
27356
27414
|
var faceRescoreActions = defineCustomActions({
|
|
27357
27415
|
/**
|
|
27358
|
-
*
|
|
27359
|
-
*
|
|
27360
|
-
*
|
|
27416
|
+
* Persists its own result onto the SUGGESTED tier (`suggestedIdentityId` /
|
|
27417
|
+
* `suggestedMatchScore`) — never an assignment, never automatic; see
|
|
27418
|
+
* `rescoreFaceForTrack` in `pipeline-analytics/index.ts` and
|
|
27419
|
+
* `persistRescoreResult` in `face-rescore.ts`. `admin` because the report
|
|
27420
|
+
* also names every enrolled person and how well an arbitrary face matches
|
|
27421
|
+
* them — the gallery's shape, which is not a viewer-level fact.
|
|
27361
27422
|
*/
|
|
27362
27423
|
"face.rescoreTrack": customAction(object({
|
|
27363
27424
|
deviceId: number().int(),
|
|
@@ -27375,8 +27436,10 @@ var faceRescoreActions = defineCustomActions({
|
|
|
27375
27436
|
/** The face's current assignment, so the UI can mark "already this person". */
|
|
27376
27437
|
assigned: boolean(),
|
|
27377
27438
|
assignedIdentityId: string().optional(),
|
|
27378
|
-
/** What the track has been SHOWING until now —
|
|
27379
|
-
*
|
|
27439
|
+
/** What the track has been SHOWING until now — read BEFORE this call
|
|
27440
|
+
* writes, so it is whatever a previous track close or a previous
|
|
27441
|
+
* `face.rescoreTrack` last persisted. Returned so the UI can put the stale
|
|
27442
|
+
* number beside the fresh one. */
|
|
27380
27443
|
storedSuggestedIdentityId: string().optional(),
|
|
27381
27444
|
storedSuggestedMatchScore: number().optional(),
|
|
27382
27445
|
verdict: FaceRescoreVerdictSchema,
|
|
@@ -29383,6 +29446,51 @@ var FrameProcessor = class {
|
|
|
29383
29446
|
}
|
|
29384
29447
|
};
|
|
29385
29448
|
//#endregion
|
|
29449
|
+
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
29450
|
+
/**
|
|
29451
|
+
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
29452
|
+
* return the tracks it produced.
|
|
29453
|
+
*
|
|
29454
|
+
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
29455
|
+
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
29456
|
+
*/
|
|
29457
|
+
function runFrameProcessorPass(input) {
|
|
29458
|
+
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
29459
|
+
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
29460
|
+
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
29461
|
+
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
29462
|
+
const accum = /* @__PURE__ */ new Map();
|
|
29463
|
+
for (const { timestamp, frame } of input.frames) {
|
|
29464
|
+
const result = processor.process({
|
|
29465
|
+
timestamp,
|
|
29466
|
+
frame
|
|
29467
|
+
});
|
|
29468
|
+
for (const td of result.tracked) {
|
|
29469
|
+
const existing = accum.get(td.trackId);
|
|
29470
|
+
const matched = td.matchedThisFrame ? 1 : 0;
|
|
29471
|
+
if (existing === void 0) accum.set(td.trackId, {
|
|
29472
|
+
className: td.className,
|
|
29473
|
+
firstSeenMs: timestamp,
|
|
29474
|
+
lastSeenMs: timestamp,
|
|
29475
|
+
bbox: { ...td.bbox },
|
|
29476
|
+
framesMatched: matched
|
|
29477
|
+
});
|
|
29478
|
+
else {
|
|
29479
|
+
existing.className = td.className;
|
|
29480
|
+
existing.lastSeenMs = timestamp;
|
|
29481
|
+
existing.framesMatched += matched;
|
|
29482
|
+
}
|
|
29483
|
+
}
|
|
29484
|
+
}
|
|
29485
|
+
return { tracks: [...accum.values()].map((t) => ({
|
|
29486
|
+
className: t.className,
|
|
29487
|
+
firstSeenMs: t.firstSeenMs,
|
|
29488
|
+
lastSeenMs: t.lastSeenMs,
|
|
29489
|
+
bbox: t.bbox,
|
|
29490
|
+
framesMatched: t.framesMatched
|
|
29491
|
+
})) };
|
|
29492
|
+
}
|
|
29493
|
+
//#endregion
|
|
29386
29494
|
//#region src/pipeline-analytics/pipeline/semantic-detail-bbox.ts
|
|
29387
29495
|
/** Which box the semantic best-shot should crop: a plate/face DETAIL box
|
|
29388
29496
|
* when we have one, otherwise the subject box. */
|
|
@@ -31799,11 +31907,13 @@ var MediaStore = class {
|
|
|
31799
31907
|
store;
|
|
31800
31908
|
logger;
|
|
31801
31909
|
writeGate;
|
|
31910
|
+
resolveEventMediaLocation;
|
|
31802
31911
|
constructor(deps) {
|
|
31803
31912
|
this.storage = deps.storage;
|
|
31804
31913
|
this.store = deps.store;
|
|
31805
31914
|
this.logger = deps.logger;
|
|
31806
31915
|
this.writeGate = deps.writeGate;
|
|
31916
|
+
this.resolveEventMediaLocation = deps.resolveEventMediaLocation;
|
|
31807
31917
|
}
|
|
31808
31918
|
gated(mutation) {
|
|
31809
31919
|
return this.writeGate ? this.writeGate.run(mutation) : mutation();
|
|
@@ -31824,7 +31934,8 @@ var MediaStore = class {
|
|
|
31824
31934
|
async putUnchecked(params) {
|
|
31825
31935
|
const key = buildKey(params);
|
|
31826
31936
|
const path = buildPath(params);
|
|
31827
|
-
const
|
|
31937
|
+
const staticLocation = locationForOwnerKind(params.ownerKind);
|
|
31938
|
+
const location = staticLocation === "eventMedia" && this.resolveEventMediaLocation ? await this.resolveEventMediaLocation(params.deviceId) : staticLocation;
|
|
31828
31939
|
const record = {
|
|
31829
31940
|
deviceId: params.deviceId,
|
|
31830
31941
|
ownerKind: params.ownerKind,
|
|
@@ -41478,6 +41589,21 @@ var MediaCaptureLogAggregator = class {
|
|
|
41478
41589
|
}
|
|
41479
41590
|
};
|
|
41480
41591
|
//#endregion
|
|
41592
|
+
//#region src/pipeline-analytics/media-locations.ts
|
|
41593
|
+
/**
|
|
41594
|
+
* Picks the location a new `eventMedia` blob should be written to, or `null`
|
|
41595
|
+
* when none is writable — the caller MUST refuse the write rather than fall
|
|
41596
|
+
* back to a read-only location, which would fail far downstream as an opaque
|
|
41597
|
+
* filesystem error instead of a named storage decision.
|
|
41598
|
+
*/
|
|
41599
|
+
function pickWritableMediaLocation(locations, currentDefaultId) {
|
|
41600
|
+
const writable = locations.filter((l) => l.enabled && !l.readOnly);
|
|
41601
|
+
if (writable.length === 0) return null;
|
|
41602
|
+
const keep = writable.find((l) => l.id === currentDefaultId);
|
|
41603
|
+
if (keep) return keep.id;
|
|
41604
|
+
return writable.toSorted((a, b) => b.headroomBytes - a.headroomBytes)[0]?.id ?? null;
|
|
41605
|
+
}
|
|
41606
|
+
//#endregion
|
|
41481
41607
|
//#region src/pipeline-analytics/media-relocate-engine.ts
|
|
41482
41608
|
var PAGE_SIZE = 200;
|
|
41483
41609
|
var DEFAULT_THROTTLE_MBPS = 40;
|
|
@@ -47548,6 +47674,37 @@ var FaceStore = class {
|
|
|
47548
47674
|
}
|
|
47549
47675
|
}
|
|
47550
47676
|
/**
|
|
47677
|
+
* Persist a `face.rescoreTrack` result onto the SUGGESTED tier — the ONE
|
|
47678
|
+
* write that explicitly-requested action makes. `suggestion: null` means
|
|
47679
|
+
* the recompute found no candidate worth offering and CLEARS both columns
|
|
47680
|
+
* (a genuine "the gallery has nothing for this face right now" result, not
|
|
47681
|
+
* a no-op left at whatever they held before). Never touches `assigned` /
|
|
47682
|
+
* `recognizedIdentityId` — a suggestion is never an assignment. The caller
|
|
47683
|
+
* is responsible for never calling this with a non-comparable (`model-
|
|
47684
|
+
* mismatch`) report; this method persists whatever it is given.
|
|
47685
|
+
*/
|
|
47686
|
+
async setSuggestion(faceId, suggestion) {
|
|
47687
|
+
const data = suggestion === null ? {
|
|
47688
|
+
suggestedIdentityId: null,
|
|
47689
|
+
suggestedMatchScore: null
|
|
47690
|
+
} : {
|
|
47691
|
+
suggestedIdentityId: suggestion.identityId,
|
|
47692
|
+
suggestedMatchScore: suggestion.score
|
|
47693
|
+
};
|
|
47694
|
+
try {
|
|
47695
|
+
await this.store.update.mutate({
|
|
47696
|
+
collection: FACES_COLLECTION,
|
|
47697
|
+
id: faceId,
|
|
47698
|
+
data
|
|
47699
|
+
});
|
|
47700
|
+
} catch (err) {
|
|
47701
|
+
this.logger.warn("FaceStore.setSuggestion failed", { meta: {
|
|
47702
|
+
faceId,
|
|
47703
|
+
error: String(err)
|
|
47704
|
+
} });
|
|
47705
|
+
}
|
|
47706
|
+
}
|
|
47707
|
+
/**
|
|
47551
47708
|
* `TrackScopedStore` contract for the retention cascade: delete the buffered
|
|
47552
47709
|
* face rows of the given tracks that are NOT assigned. Reuses the EXACT
|
|
47553
47710
|
* `assigned` exemption that `prune`/`pruneAll` apply (see the
|
|
@@ -56971,6 +57128,144 @@ function rowFromMetadata(id, metadata) {
|
|
|
56971
57128
|
};
|
|
56972
57129
|
}
|
|
56973
57130
|
//#endregion
|
|
57131
|
+
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
57132
|
+
/** How long a mirrored entry is served before a refresh is started behind the
|
|
57133
|
+
* next reader. See the module doc for why this matches the per-kind caches'
|
|
57134
|
+
* own window rather than introducing a second staleness bound. */
|
|
57135
|
+
var RAW_SETTINGS_STALE_MS = 5e3;
|
|
57136
|
+
/**
|
|
57137
|
+
* How long a FAILED refresh is not retried.
|
|
57138
|
+
*
|
|
57139
|
+
* A refresh that throws leaves `fetchedAt` where it was, so without a
|
|
57140
|
+
* cooldown every subsequent reader would start another attempt — a broken
|
|
57141
|
+
* settings store would be hammered at frame rate by the very mechanism built
|
|
57142
|
+
* to stop that.
|
|
57143
|
+
*/
|
|
57144
|
+
var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
|
|
57145
|
+
/**
|
|
57146
|
+
* The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
|
|
57147
|
+
* fresh OFF every per-kind settings cache's hot path.
|
|
57148
|
+
*
|
|
57149
|
+
* `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
|
|
57150
|
+
* (or frames) missing on the same device in the same tick costs ONE read,
|
|
57151
|
+
* not one per caller — the exact failure this class exists to remove.
|
|
57152
|
+
*/
|
|
57153
|
+
var RawDeviceBlobMirror = class {
|
|
57154
|
+
deps;
|
|
57155
|
+
staleMs;
|
|
57156
|
+
missCooldownMs;
|
|
57157
|
+
entries = /* @__PURE__ */ new Map();
|
|
57158
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
57159
|
+
now;
|
|
57160
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
57161
|
+
this.deps = deps;
|
|
57162
|
+
this.staleMs = staleMs;
|
|
57163
|
+
this.missCooldownMs = missCooldownMs;
|
|
57164
|
+
this.now = deps.now ?? (() => Date.now());
|
|
57165
|
+
}
|
|
57166
|
+
/**
|
|
57167
|
+
* The device's raw stored blob.
|
|
57168
|
+
*
|
|
57169
|
+
* COLD awaits — the first settings kind to touch a device has to get the
|
|
57170
|
+
* real answer. WARM returns immediately, stale or not, and starts the
|
|
57171
|
+
* refresh behind the caller.
|
|
57172
|
+
*/
|
|
57173
|
+
async resolve(deviceId) {
|
|
57174
|
+
const entry = this.entries.get(deviceId);
|
|
57175
|
+
if (entry === void 0) return this.load(deviceId);
|
|
57176
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
|
|
57177
|
+
return entry.value;
|
|
57178
|
+
}
|
|
57179
|
+
/** Drop a device's entry. Called by every in-process writer of this
|
|
57180
|
+
* device's row, so an operator's own change is never served stale. */
|
|
57181
|
+
invalidate(deviceId) {
|
|
57182
|
+
this.entries.delete(deviceId);
|
|
57183
|
+
}
|
|
57184
|
+
invalidateAll() {
|
|
57185
|
+
this.entries.clear();
|
|
57186
|
+
}
|
|
57187
|
+
async load(deviceId) {
|
|
57188
|
+
const pending = this.inFlight.get(deviceId);
|
|
57189
|
+
if (pending !== void 0) return pending;
|
|
57190
|
+
const run = this.deps.read(deviceId).then((value) => {
|
|
57191
|
+
this.entries.set(deviceId, {
|
|
57192
|
+
value,
|
|
57193
|
+
fetchedAt: this.now()
|
|
57194
|
+
});
|
|
57195
|
+
return value;
|
|
57196
|
+
}).catch((err) => {
|
|
57197
|
+
this.deps.onRefreshFailed?.(deviceId, err);
|
|
57198
|
+
const existing = this.entries.get(deviceId);
|
|
57199
|
+
if (existing !== void 0) {
|
|
57200
|
+
existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
|
|
57201
|
+
return existing.value;
|
|
57202
|
+
}
|
|
57203
|
+
return {};
|
|
57204
|
+
}).finally(() => {
|
|
57205
|
+
this.inFlight.delete(deviceId);
|
|
57206
|
+
});
|
|
57207
|
+
this.inFlight.set(deviceId, run);
|
|
57208
|
+
return run;
|
|
57209
|
+
}
|
|
57210
|
+
};
|
|
57211
|
+
/**
|
|
57212
|
+
* The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
|
|
57213
|
+
* kept fresh OFF every per-kind settings cache's hot path. Same shape as
|
|
57214
|
+
* {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
|
|
57215
|
+
* one addon store per process.
|
|
57216
|
+
*/
|
|
57217
|
+
var RawGlobalStoreMirror = class {
|
|
57218
|
+
deps;
|
|
57219
|
+
staleMs;
|
|
57220
|
+
missCooldownMs;
|
|
57221
|
+
entry = null;
|
|
57222
|
+
inFlight = null;
|
|
57223
|
+
now;
|
|
57224
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
57225
|
+
this.deps = deps;
|
|
57226
|
+
this.staleMs = staleMs;
|
|
57227
|
+
this.missCooldownMs = missCooldownMs;
|
|
57228
|
+
this.now = deps.now ?? (() => Date.now());
|
|
57229
|
+
}
|
|
57230
|
+
async resolve() {
|
|
57231
|
+
const entry = this.entry;
|
|
57232
|
+
if (entry === null) return this.load();
|
|
57233
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
|
|
57234
|
+
return entry.value;
|
|
57235
|
+
}
|
|
57236
|
+
/** Drop the mirrored store. Called by every in-process global-settings
|
|
57237
|
+
* writer (`onConfigChanged`), so an operator's own change is never served
|
|
57238
|
+
* stale. */
|
|
57239
|
+
invalidate() {
|
|
57240
|
+
this.entry = null;
|
|
57241
|
+
}
|
|
57242
|
+
async load() {
|
|
57243
|
+
if (this.inFlight !== null) return this.inFlight;
|
|
57244
|
+
const run = this.deps.read().then((value) => {
|
|
57245
|
+
this.entry = {
|
|
57246
|
+
value,
|
|
57247
|
+
fetchedAt: this.now()
|
|
57248
|
+
};
|
|
57249
|
+
return value;
|
|
57250
|
+
}).catch((err) => {
|
|
57251
|
+
this.deps.onRefreshFailed?.(err);
|
|
57252
|
+
const existing = this.entry;
|
|
57253
|
+
if (existing !== null) {
|
|
57254
|
+
this.entry = {
|
|
57255
|
+
value: existing.value,
|
|
57256
|
+
fetchedAt: this.now() - this.staleMs + this.missCooldownMs
|
|
57257
|
+
};
|
|
57258
|
+
return existing.value;
|
|
57259
|
+
}
|
|
57260
|
+
return {};
|
|
57261
|
+
}).finally(() => {
|
|
57262
|
+
this.inFlight = null;
|
|
57263
|
+
});
|
|
57264
|
+
this.inFlight = run;
|
|
57265
|
+
return run;
|
|
57266
|
+
}
|
|
57267
|
+
};
|
|
57268
|
+
//#endregion
|
|
56974
57269
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
56975
57270
|
/**
|
|
56976
57271
|
* Sweep every device with persisted tracks: skip retention-disabled devices,
|
|
@@ -58027,6 +58322,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58027
58322
|
/** Event-media relocation engine (entity-routing Phase 4). */
|
|
58028
58323
|
mediaRelocate = null;
|
|
58029
58324
|
mediaLocationStorage = null;
|
|
58325
|
+
/**
|
|
58326
|
+
* Cached pick for NEW `eventMedia` writes (`pickWritableMediaLocation`).
|
|
58327
|
+
* Resolved once, never per blob (no-RPC-per-blob, same discipline as
|
|
58328
|
+
* `mediaLocationStorage`'s own root cache) — dropped together with it by
|
|
58329
|
+
* `refreshStorageLocationsForMigration`, since a location's `readOnly` /
|
|
58330
|
+
* `enabled` flags only change via operator reconfig.
|
|
58331
|
+
*/
|
|
58332
|
+
eventMediaWriteLocationCache = null;
|
|
58030
58333
|
mediaWriteGate = new MediaWriteGate();
|
|
58031
58334
|
storageMigrationLeaseId = null;
|
|
58032
58335
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
@@ -58426,6 +58729,33 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58426
58729
|
audioEpisodeTracks = /* @__PURE__ */ new Map();
|
|
58427
58730
|
audioEpisodeLastPersistMs = /* @__PURE__ */ new Map();
|
|
58428
58731
|
settingsCaches = new DeviceSettingsCacheHub();
|
|
58732
|
+
/**
|
|
58733
|
+
* The addon-level store and per-device blob, mirrored ONCE and shared by
|
|
58734
|
+
* every settings kind above — see `store/settings-raw-mirror.ts`. Before
|
|
58735
|
+
* this, each kind's cache miss called `resolveGlobalStore()` +
|
|
58736
|
+
* `readDeviceBlob()` directly, so eight kinds expiring in the same window
|
|
58737
|
+
* paid for the same two reads eight times: `get
|
|
58738
|
+
* pipeline-analytics:addon-settings` at 547–694/min, `get
|
|
58739
|
+
* pipeline-analytics:addon-devices` at ~700/min, live 2026-08-26. Invalidated
|
|
58740
|
+
* by every in-process writer: `applyDeviceSettingsPatch` and the retired-
|
|
58741
|
+
* setting purges drop the device mirror, `onConfigChanged` (this process's
|
|
58742
|
+
* own `updateGlobalSettings`) drops the global mirror.
|
|
58743
|
+
*/
|
|
58744
|
+
rawDeviceBlobMirror = new RawDeviceBlobMirror({
|
|
58745
|
+
read: (deviceId) => this.readDeviceBlob(deviceId),
|
|
58746
|
+
onRefreshFailed: (deviceId, err) => {
|
|
58747
|
+
this.ctx.logger.debug("raw device-blob mirror refresh failed — serving last value", {
|
|
58748
|
+
tags: { deviceId },
|
|
58749
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
58750
|
+
});
|
|
58751
|
+
}
|
|
58752
|
+
});
|
|
58753
|
+
rawGlobalStoreMirror = new RawGlobalStoreMirror({
|
|
58754
|
+
read: () => this.resolveGlobalStore(),
|
|
58755
|
+
onRefreshFailed: (err) => {
|
|
58756
|
+
this.ctx.logger.debug("raw global-store mirror refresh failed — serving last value", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
58757
|
+
}
|
|
58758
|
+
});
|
|
58429
58759
|
/** Avoids a store read every ~30 Hz audio frame. */
|
|
58430
58760
|
audioSettingsCache = this.settingsCaches.registerDeviceCache("audio-detection", SETTINGS_CACHE_TTL_MS);
|
|
58431
58761
|
sensitivitySettingsCache = this.settingsCaches.registerDeviceCache("detection-sensitivity", SETTINGS_CACHE_TTL_MS);
|
|
@@ -59145,6 +59475,53 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
59145
59475
|
this.mediaLocationStorage = routed;
|
|
59146
59476
|
return routed;
|
|
59147
59477
|
}
|
|
59478
|
+
/**
|
|
59479
|
+
* Resolves which `eventMedia` location a NEW footage-class blob should be
|
|
59480
|
+
* written to (`MediaStoreDeps.resolveEventMediaLocation`). Reflects
|
|
59481
|
+
* `config.readOnly` — an operator draining a location marks it read-only,
|
|
59482
|
+
* and new writes must never land there (the relocate mover, exposed as
|
|
59483
|
+
* `relocateMedia`, empties what is already on it in the background).
|
|
59484
|
+
*
|
|
59485
|
+
* Cached after the first successful resolution — this is a per-blob write
|
|
59486
|
+
* path, and the write-side provider is a declared no-RPC-per-blob bypass
|
|
59487
|
+
* (D-note in `location-aware-media-storage.ts`); re-querying the storage
|
|
59488
|
+
* cap on every blob would undo that. Dropped by
|
|
59489
|
+
* `refreshStorageLocationsForMigration`, same trigger as
|
|
59490
|
+
* `mediaLocationStorage.invalidateRoots()`.
|
|
59491
|
+
*
|
|
59492
|
+
* Degrades to the bare `eventMedia` type ref (pre-2026-08-26 behaviour)
|
|
59493
|
+
* when the storage cap itself is unreachable — a transient RPC failure (or
|
|
59494
|
+
* a test harness that never wires `api.storage`) must not turn every media
|
|
59495
|
+
* write into a hard failure the way "no writable location" does.
|
|
59496
|
+
*/
|
|
59497
|
+
async resolveEventMediaWriteLocation(api, logger, deviceId) {
|
|
59498
|
+
const cached = this.eventMediaWriteLocationCache;
|
|
59499
|
+
if (cached !== null) return cached;
|
|
59500
|
+
let locations;
|
|
59501
|
+
try {
|
|
59502
|
+
locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
|
|
59503
|
+
} catch (err) {
|
|
59504
|
+
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg(err) } });
|
|
59505
|
+
this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
|
|
59506
|
+
return DEFAULT_MEDIA_LOCATION;
|
|
59507
|
+
}
|
|
59508
|
+
const picked = pickWritableMediaLocation(locations.map((l) => ({
|
|
59509
|
+
id: l.id,
|
|
59510
|
+
readOnly: l.config["readOnly"] === true,
|
|
59511
|
+
enabled: l.enabled !== false,
|
|
59512
|
+
headroomBytes: l.capacity?.availableBytes ?? 0
|
|
59513
|
+
})), locations.find((l) => l.isDefault)?.id ?? "eventMedia");
|
|
59514
|
+
if (picked === null) {
|
|
59515
|
+
const seen = locations.length > 0 ? locations.map((l) => l.id).join(", ") : "(none registered)";
|
|
59516
|
+
logger.error("pipeline-analytics: no writable eventMedia storage location — refusing the write instead of failing far downstream as a filesystem error", {
|
|
59517
|
+
tags: { deviceId },
|
|
59518
|
+
meta: { locationsSeen: seen }
|
|
59519
|
+
});
|
|
59520
|
+
throw new Error(`no writable eventMedia storage location (seen: ${seen})`);
|
|
59521
|
+
}
|
|
59522
|
+
this.eventMediaWriteLocationCache = picked;
|
|
59523
|
+
return picked;
|
|
59524
|
+
}
|
|
59148
59525
|
/** Constructs every SQLite-backed store plus the stationary/package-drop/
|
|
59149
59526
|
* sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
|
|
59150
59527
|
* so the rest of boot consumes the stores without re-narrowing. */
|
|
@@ -59203,7 +59580,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
59203
59580
|
storage,
|
|
59204
59581
|
store: api.settingsStore,
|
|
59205
59582
|
logger: logger.child("MediaStore"),
|
|
59206
|
-
writeGate: this.mediaWriteGate
|
|
59583
|
+
writeGate: this.mediaWriteGate,
|
|
59584
|
+
resolveEventMediaLocation: (deviceId) => this.resolveEventMediaWriteLocation(api, logger, deviceId)
|
|
59207
59585
|
});
|
|
59208
59586
|
this.mediaStore = mediaStore;
|
|
59209
59587
|
const retrainFrames = new RetrainFrameStore({
|
|
@@ -60236,6 +60614,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60236
60614
|
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
|
|
60237
60615
|
});
|
|
60238
60616
|
}
|
|
60617
|
+
/**
|
|
60618
|
+
* The FrameProcessor pass of a replay run — see the cap doc on
|
|
60619
|
+
* `runReplayFrameProcessor` for why this is not the `processFrame` method
|
|
60620
|
+
* this addon deliberately does not have.
|
|
60621
|
+
*
|
|
60622
|
+
* `runFrameProcessorPass` is pure w.r.t. this addon's own state: it
|
|
60623
|
+
* constructs a FRESH `FrameProcessor` and does not touch `this.trackStore`,
|
|
60624
|
+
* `this.stationaryRegistry` or any other instance field — unlike
|
|
60625
|
+
* `getOrCreateProcessor` (the production path), which wires both.
|
|
60626
|
+
* `scripts/check-replay-isolation.ts` holds that boundary on the
|
|
60627
|
+
* `replay/` directory this delegates into.
|
|
60628
|
+
*/
|
|
60629
|
+
async runReplayFrameProcessor(input) {
|
|
60630
|
+
return runFrameProcessorPass(input);
|
|
60631
|
+
}
|
|
60239
60632
|
async listRetrainAnnotations(input) {
|
|
60240
60633
|
return this.requireRetrain().listAnnotations(input.trackId);
|
|
60241
60634
|
}
|
|
@@ -60511,6 +60904,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60511
60904
|
this.forgetDeviceProcessors(data.deviceId);
|
|
60512
60905
|
this.levelStateByDevice.delete(data.deviceId);
|
|
60513
60906
|
this.settingsCaches.clearDevice(data.deviceId);
|
|
60907
|
+
this.rawDeviceBlobMirror.invalidate(data.deviceId);
|
|
60514
60908
|
}
|
|
60515
60909
|
}
|
|
60516
60910
|
/** `DeviceUnregistered` handler — subscription topology lives in
|
|
@@ -60535,6 +60929,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60535
60929
|
}
|
|
60536
60930
|
this.audioMarkers?.forgetDevice(deviceId);
|
|
60537
60931
|
this.settingsCaches.clearDevice(deviceId);
|
|
60932
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
60538
60933
|
this.bindingCache?.invalidate(deviceId);
|
|
60539
60934
|
this.detectionCoverage?.forget(deviceId);
|
|
60540
60935
|
this.zoneAnalytics?.forgetDevice(deviceId);
|
|
@@ -60963,6 +61358,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60963
61358
|
}] : []
|
|
60964
61359
|
];
|
|
60965
61360
|
}
|
|
61361
|
+
/**
|
|
61362
|
+
* `BaseAddon.updateGlobalSettings` calls this after the write lands —
|
|
61363
|
+
* gated to the LOCAL node's own write (a sibling-node write returns before
|
|
61364
|
+
* reaching it), which is exactly the scope `rawGlobalStoreMirror` needs to
|
|
61365
|
+
* invalidate: the mirror only ever answers for THIS process, so it only
|
|
61366
|
+
* needs to drop on THIS process's own write (D224).
|
|
61367
|
+
*/
|
|
61368
|
+
async onConfigChanged() {
|
|
61369
|
+
await super.onConfigChanged();
|
|
61370
|
+
this.rawGlobalStoreMirror.invalidate();
|
|
61371
|
+
}
|
|
60966
61372
|
async onShutdown() {
|
|
60967
61373
|
this.shuttingDown = true;
|
|
60968
61374
|
this.sceneEngine?.stop();
|
|
@@ -61018,6 +61424,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61018
61424
|
this.gatedNodeByDevice.clear();
|
|
61019
61425
|
this.levelStateByDevice.clear();
|
|
61020
61426
|
this.settingsCaches.clearAll();
|
|
61427
|
+
this.rawDeviceBlobMirror.invalidateAll();
|
|
61428
|
+
this.rawGlobalStoreMirror.invalidate();
|
|
61021
61429
|
this.confirmationDetectorStepByNode.clear();
|
|
61022
61430
|
this.trackStore?.clearAll();
|
|
61023
61431
|
this.stationaryRegistry = null;
|
|
@@ -61730,10 +62138,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61730
62138
|
* auto-assigning — see `__tests__/global-settings-cascade.spec.ts`).
|
|
61731
62139
|
* `resolveGlobalStore()` is the sanctioned BaseAddon read (per-node
|
|
61732
62140
|
* projection applied; a raw `readAddonStore` would leak `@node` keys if a
|
|
61733
|
-
* perNode field is ever declared).
|
|
62141
|
+
* perNode field is ever declared).
|
|
62142
|
+
*
|
|
62143
|
+
* Both raw reads go through `rawGlobalStoreMirror` / `rawDeviceBlobMirror`
|
|
62144
|
+
* rather than straight to the store: this method is the ONE place all
|
|
62145
|
+
* eight settings kinds funnel through on a cache miss, so calling the raw
|
|
62146
|
+
* reads directly here means eight kinds expiring in the same window each
|
|
62147
|
+
* pay for the same two reads — see `store/settings-raw-mirror.ts`. */
|
|
61734
62148
|
async readDeviceSettings(deviceId, resolve) {
|
|
61735
|
-
const globalRaw = this.ctxIfReady !== null ? await this.
|
|
61736
|
-
const deviceRaw = await this.
|
|
62149
|
+
const globalRaw = this.ctxIfReady !== null ? await this.rawGlobalStoreMirror.resolve() : {};
|
|
62150
|
+
const deviceRaw = await this.rawDeviceBlobMirror.resolve(deviceId);
|
|
61737
62151
|
return resolve({
|
|
61738
62152
|
...globalRaw,
|
|
61739
62153
|
...deviceRaw
|
|
@@ -63404,6 +63818,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
63404
63818
|
});
|
|
63405
63819
|
}
|
|
63406
63820
|
await settings.writeDeviceStore(deviceId, plan.patch);
|
|
63821
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
63407
63822
|
} catch (err) {
|
|
63408
63823
|
this.retiredSettingsChecked.delete(deviceId);
|
|
63409
63824
|
this.ctx.logger.warn("retired retention settings migration failed — will retry", {
|
|
@@ -63475,6 +63890,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
63475
63890
|
deviceId
|
|
63476
63891
|
});
|
|
63477
63892
|
await settings.writeDeviceStore(deviceId, plan.patch);
|
|
63893
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
63478
63894
|
} catch (err) {
|
|
63479
63895
|
this.storedSuggestionCheckedDevices.delete(deviceId);
|
|
63480
63896
|
this.ctx.logger.warn("face suggestion threshold purge failed — will retry", {
|
|
@@ -65162,6 +65578,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
65162
65578
|
* See `face-rescore.ts` for why the number already on the track cannot answer
|
|
65163
65579
|
* that: it was computed during live detection and persisted at track close,
|
|
65164
65580
|
* so every enrolment made since is invisible to it.
|
|
65581
|
+
*
|
|
65582
|
+
* The report's result is PERSISTED (`persistRescoreResult`, below) onto
|
|
65583
|
+
* `Face.suggestedIdentityId` / `suggestedMatchScore` — the SAME columns the
|
|
65584
|
+
* live recognizer's suggestion band writes, so a reader never has to know
|
|
65585
|
+
* whether a suggestion came from track close or from this button. Still
|
|
65586
|
+
* never a background job: this write happens once, only because the
|
|
65587
|
+
* operator asked THIS question right now.
|
|
65165
65588
|
*/
|
|
65166
65589
|
async rescoreFaceForTrack(input) {
|
|
65167
65590
|
const api = this.ctx.api;
|
|
@@ -65204,6 +65627,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
65204
65627
|
clusterModelId,
|
|
65205
65628
|
params
|
|
65206
65629
|
});
|
|
65630
|
+
const persisted = await persistRescoreResult(faceId, report, { setSuggestion: (id, suggestion) => faceStore.setSuggestion(id, suggestion) });
|
|
65207
65631
|
this.ctx.logger.info("face rescore", {
|
|
65208
65632
|
tags: { deviceId: input.deviceId },
|
|
65209
65633
|
meta: {
|
|
@@ -65217,7 +65641,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
65217
65641
|
best: report.identities[0]?.name ?? null,
|
|
65218
65642
|
bestScore: report.identities[0]?.score ?? null,
|
|
65219
65643
|
storedSuggestion: face.suggestedMatchScore ?? null,
|
|
65220
|
-
reembedded: reembed.succeeded
|
|
65644
|
+
reembedded: reembed.succeeded,
|
|
65645
|
+
persisted
|
|
65221
65646
|
}
|
|
65222
65647
|
});
|
|
65223
65648
|
return {
|
|
@@ -65621,6 +66046,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
65621
66046
|
async refreshStorageLocationsForMigration(input) {
|
|
65622
66047
|
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
|
|
65623
66048
|
this.mediaLocationStorage?.invalidateRoots();
|
|
66049
|
+
this.eventMediaWriteLocationCache = null;
|
|
65624
66050
|
return { refreshed: true };
|
|
65625
66051
|
}
|
|
65626
66052
|
async startStorageMigrationMove(input) {
|
|
@@ -65634,6 +66060,32 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
65634
66060
|
async cancelStorageMigrationMove(input) {
|
|
65635
66061
|
return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
|
|
65636
66062
|
}
|
|
66063
|
+
/**
|
|
66064
|
+
* Move event media between locations — the OPERATOR's mover, no lease.
|
|
66065
|
+
* Twin of `recording.relocateFootage`: same engine (`this.mediaRelocate`)
|
|
66066
|
+
* the lease-gated coordinator move uses, runnable in the background
|
|
66067
|
+
* without pausing anything. `startStorageMigrationMove` above stays
|
|
66068
|
+
* lease-gated for the orchestrated migration; this method must never touch
|
|
66069
|
+
* `storageMigrationLeaseId`.
|
|
66070
|
+
*/
|
|
66071
|
+
async relocateMedia(input) {
|
|
66072
|
+
if (!this.mediaRelocate) throw new Error("media relocation unavailable");
|
|
66073
|
+
const jobId = this.mediaRelocate.start(input);
|
|
66074
|
+
this.ctx.logger.info("pipeline-analytics: media relocate job enqueued", { meta: {
|
|
66075
|
+
jobId,
|
|
66076
|
+
to: input.toLocationId
|
|
66077
|
+
} });
|
|
66078
|
+
return { jobId };
|
|
66079
|
+
}
|
|
66080
|
+
/** Every relocate job this addon knows about, newest first (in RAM: the
|
|
66081
|
+
* move is resumable, so a lost list costs nothing but the display). */
|
|
66082
|
+
async listRelocateMediaJobs() {
|
|
66083
|
+
return this.mediaRelocate?.list() ?? [];
|
|
66084
|
+
}
|
|
66085
|
+
/** Cancel a running or queued relocate job. */
|
|
66086
|
+
async cancelRelocateMedia(input) {
|
|
66087
|
+
return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
|
|
66088
|
+
}
|
|
65637
66089
|
async listOpsLog(input) {
|
|
65638
66090
|
return this.queryFacade.listOpsLog(input);
|
|
65639
66091
|
}
|
|
@@ -66027,6 +66479,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
66027
66479
|
}
|
|
66028
66480
|
async applyDeviceSettingsPatch(input) {
|
|
66029
66481
|
await this.updateDeviceSettings(input.deviceId, input.patch);
|
|
66482
|
+
this.rawDeviceBlobMirror.invalidate(input.deviceId);
|
|
66030
66483
|
this.audioSettingsCache.delete(input.deviceId);
|
|
66031
66484
|
this.sensitivitySettingsCache.delete(input.deviceId);
|
|
66032
66485
|
this.trackingSettingsCache.delete(input.deviceId);
|