@camstack/addon-post-analysis 1.2.101 → 1.2.102
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-BQsMSX2A.js → dist-BwxXdV9d.js} +31 -51
- package/dist/{dist-ecY_-nDx.mjs → dist-CZm8slPB.mjs} +31 -51
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D8HLGYbH.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-MP5xue-m.mjs} +2 -2
- package/dist/pipeline-analytics/{hostInit-Br0v_V7W.mjs → hostInit-CkptLue6.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +448 -54
- package/dist/pipeline-analytics/index.mjs +448 -54
- 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-BwxXdV9d.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");
|
|
@@ -8359,8 +8359,11 @@ var en_default = {
|
|
|
8359
8359
|
"one": "{{count}} addon update available",
|
|
8360
8360
|
"other": "{{count}} addon updates available"
|
|
8361
8361
|
},
|
|
8362
|
+
"system.addon-update-available.title.onNode": {
|
|
8363
|
+
"one": "{{count}} addon update available on {{nodeId}}",
|
|
8364
|
+
"other": "{{count}} addon updates available on {{nodeId}}"
|
|
8365
|
+
},
|
|
8362
8366
|
"system.addon-update-available.body": "{{list}}",
|
|
8363
|
-
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
8364
8367
|
"system.server-update-available.title": {
|
|
8365
8368
|
"one": "Server update available on {{count}} node",
|
|
8366
8369
|
"other": "Server update available on {{count}} nodes"
|
|
@@ -8375,6 +8378,7 @@ var en_default = {
|
|
|
8375
8378
|
"system.addon-updated.body": "{{list}}",
|
|
8376
8379
|
"system.server-updated.title": "Server updated",
|
|
8377
8380
|
"system.server-updated.body": "{{packageName}} on {{nodeId}} is now {{toVersion}} (was {{fromVersion}})",
|
|
8381
|
+
"system.server-updated.body.toOnly": "{{packageName}} on {{nodeId}} is now {{toVersion}}",
|
|
8378
8382
|
"system.server-updated.body.plain": "{{packageName}} on {{nodeId}} was updated",
|
|
8379
8383
|
"system.export-completed.title": "Export ready",
|
|
8380
8384
|
"system.export-completed.body": "The footage export is ready",
|
|
@@ -8573,8 +8577,11 @@ var it_default = {
|
|
|
8573
8577
|
"one": "{{count}} aggiornamento addon disponibile",
|
|
8574
8578
|
"other": "{{count}} aggiornamenti addon disponibili"
|
|
8575
8579
|
},
|
|
8580
|
+
"system.addon-update-available.title.onNode": {
|
|
8581
|
+
"one": "{{count}} aggiornamento addon disponibile su {{nodeId}}",
|
|
8582
|
+
"other": "{{count}} aggiornamenti addon disponibili su {{nodeId}}"
|
|
8583
|
+
},
|
|
8576
8584
|
"system.addon-update-available.body": "{{list}}",
|
|
8577
|
-
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
8578
8585
|
"system.server-update-available.title": {
|
|
8579
8586
|
"one": "Aggiornamento server disponibile su {{count}} nodo",
|
|
8580
8587
|
"other": "Aggiornamento server disponibile su {{count}} nodi"
|
|
@@ -8589,6 +8596,7 @@ var it_default = {
|
|
|
8589
8596
|
"system.addon-updated.body": "{{list}}",
|
|
8590
8597
|
"system.server-updated.title": "Server aggiornato",
|
|
8591
8598
|
"system.server-updated.body": "{{packageName}} sul nodo {{nodeId}} è ora {{toVersion}} (era {{fromVersion}})",
|
|
8599
|
+
"system.server-updated.body.toOnly": "{{packageName}} sul nodo {{nodeId}} è ora {{toVersion}}",
|
|
8592
8600
|
"system.server-updated.body.plain": "{{packageName}} sul nodo {{nodeId}} è stato aggiornato",
|
|
8593
8601
|
"system.export-completed.title": "Export pronto",
|
|
8594
8602
|
"system.export-completed.body": "L'export del filmato è pronto",
|
|
@@ -10411,15 +10419,15 @@ function incomingFromUpdateAvailable(facts, data) {
|
|
|
10411
10419
|
const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== void 0));
|
|
10412
10420
|
const packageNames = unique(packages.map((pkg) => pkg.packageName));
|
|
10413
10421
|
const signature = packages.map((pkg) => `${pkg.nodeId ?? ""}:${pkg.packageName}@${pkg.latestVersion}`).toSorted().join(",");
|
|
10414
|
-
const list = packages.map(formatAvailableLine).join("\n");
|
|
10415
|
-
const nodes = nodeIds.join(", ");
|
|
10416
10422
|
const head = packages[0];
|
|
10417
10423
|
const oneNode = nodeIds.length === 1 ? nodeIds[0] : void 0;
|
|
10424
|
+
const list = packages.map((pkg) => formatAvailableLine(pkg, oneNode !== void 0)).join("\n");
|
|
10425
|
+
const nodes = nodeIds.join(", ");
|
|
10418
10426
|
return systemSubject({
|
|
10419
10427
|
facts,
|
|
10420
10428
|
kind,
|
|
10421
10429
|
subject: addon ? `packages:addon:${signature}` : `packages:server:${signature}`,
|
|
10422
|
-
titleKey: `system.${kind}.title`,
|
|
10430
|
+
titleKey: addon && oneNode !== void 0 ? `system.${kind}.title.onNode` : `system.${kind}.title`,
|
|
10423
10431
|
bodyKey: !addon && oneNode !== void 0 ? `system.${kind}.body.onNode` : !addon && nodeIds.length > 1 ? `system.${kind}.body.onNodes` : `system.${kind}.body`,
|
|
10424
10432
|
textParams: {
|
|
10425
10433
|
list,
|
|
@@ -10478,12 +10486,12 @@ function incomingFromAddonUpdates(facts, updates) {
|
|
|
10478
10486
|
function incomingFromServerUpdated(facts, data) {
|
|
10479
10487
|
const kind = stringField(data, "kind");
|
|
10480
10488
|
const requestedBy = stringField(data, "requestedBy");
|
|
10481
|
-
if (!(kind === "framework-update" || kind === "framework-bulk-update" || requestedBy !== void 0 && requestedBy.startsWith("server-update:"))) return null;
|
|
10482
10489
|
const packageName = stringField(data, "packageName") ?? "@camstack/server";
|
|
10483
10490
|
const fromVersion = stringField(data, "fromVersion");
|
|
10484
10491
|
const toVersion = stringField(data, "toVersion") ?? versionFromRequestedBy(requestedBy);
|
|
10492
|
+
if (!(kind === "framework-update" || kind === "framework-bulk-update" || requestedBy !== void 0 && requestedBy.startsWith("server-update:") && toVersion !== void 0)) return null;
|
|
10485
10493
|
const nodeId = stringField(data, "nodeId") ?? "hub";
|
|
10486
|
-
const bodyKey =
|
|
10494
|
+
const bodyKey = toVersion === void 0 ? "system.server-updated.body.plain" : fromVersion === void 0 ? "system.server-updated.body.toOnly" : "system.server-updated.body";
|
|
10487
10495
|
return systemSubject({
|
|
10488
10496
|
facts,
|
|
10489
10497
|
kind: "server-updated",
|
|
@@ -10564,9 +10572,15 @@ function updatePackages(data) {
|
|
|
10564
10572
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10565
10573
|
}];
|
|
10566
10574
|
}
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10575
|
+
/**
|
|
10576
|
+
* One line of an availability list — the SAME shape the completed-update list
|
|
10577
|
+
* uses (`incomingFromAddonUpdates`), and deliberately word-free: the list is a
|
|
10578
|
+
* frozen param, so any English in it ("current") is delivered verbatim to an
|
|
10579
|
+
* Italian household. An arrow is not a language.
|
|
10580
|
+
*/
|
|
10581
|
+
function formatAvailableLine(pkg, nodeNamedInTitle) {
|
|
10582
|
+
const line = `${pkg.packageName} ${pkg.currentVersion} → ${pkg.latestVersion}`;
|
|
10583
|
+
return pkg.nodeId !== void 0 && !nodeNamedInTitle ? `${line} (${pkg.nodeId})` : line;
|
|
10570
10584
|
}
|
|
10571
10585
|
function versionFromRequestedBy(requestedBy) {
|
|
10572
10586
|
if (requestedBy === void 0) return void 0;
|
|
@@ -28811,19 +28825,29 @@ var BEST_FRAME_POLICY = {
|
|
|
28811
28825
|
sizeAware: true
|
|
28812
28826
|
};
|
|
28813
28827
|
/**
|
|
28814
|
-
* Best FACE (face-recognizer hold buffer) —
|
|
28815
|
-
*
|
|
28816
|
-
*
|
|
28817
|
-
*
|
|
28818
|
-
*
|
|
28819
|
-
*
|
|
28820
|
-
*
|
|
28828
|
+
* Best FACE (face-recognizer hold buffer) — SIZE-AWARE, no hysteresis, no rate
|
|
28829
|
+
* limit.
|
|
28830
|
+
*
|
|
28831
|
+
* No hysteresis / no gap, unchanged: accepting a new best only swaps an
|
|
28832
|
+
* already-decoded crop buffer in memory (cheap; no capture is triggered), and a
|
|
28833
|
+
* track may live well under the best-frame rate-limit window, so a gap would
|
|
28834
|
+
* silently discard the real peak for nothing.
|
|
28835
|
+
*
|
|
28836
|
+
* `sizeAware` since 2026-08-19. It used to be confidence-ONLY, and that was the
|
|
28837
|
+
* wrong ranking: SCRFD saturates its confidence on surveillance footage, so a
|
|
28838
|
+
* 12 px face and a 40 px face both answer ~0.9 and the track simply kept
|
|
28839
|
+
* whichever arrived first. The size that matters is the face's DETECTION-FRAME
|
|
28840
|
+
* short side, because that bounds the landmark precision the alignment warp
|
|
28841
|
+
* consumes — `face-size-score.ts` carries the measurement that set the
|
|
28842
|
+
* reference. Because `minGapMs` is 0 here, enabling this does NOT switch on the
|
|
28843
|
+
* decisive-improvement rate-limit bypass (there is no rate limit to bypass); it
|
|
28844
|
+
* only adds the size factor to the ranking.
|
|
28821
28845
|
*/
|
|
28822
28846
|
var BEST_FACE_POLICY = {
|
|
28823
28847
|
hysteresis: 0,
|
|
28824
28848
|
minGapMs: 0,
|
|
28825
28849
|
edgeAware: false,
|
|
28826
|
-
sizeAware:
|
|
28850
|
+
sizeAware: true
|
|
28827
28851
|
};
|
|
28828
28852
|
/**
|
|
28829
28853
|
* Best OBJECT (CLIP) embedding + its tight crop — confidence-only in-memory
|
|
@@ -32248,7 +32272,11 @@ var AudioDetectionSettingsSchema = require_dist.object({
|
|
|
32248
32272
|
* thresholds, is what bounds the worst case — a mis-tuned threshold can
|
|
32249
32273
|
* only ever cost the operator this many markers a day. */
|
|
32250
32274
|
audioMarkerMaxPerDay: require_dist.number().int().positive().default(12),
|
|
32251
|
-
/** Materialize one event + one synthetic track per classified episode.
|
|
32275
|
+
/** Materialize one event + one synthetic track per classified episode.
|
|
32276
|
+
* OFF switches off the GROUPING only — classified audio still persists an
|
|
32277
|
+
* event (class change, then one per 5 s heartbeat) and still reaches the
|
|
32278
|
+
* Notification Center; it just opens no synthetic track. A knob that names
|
|
32279
|
+
* the grouping must never silence the path (D62). */
|
|
32252
32280
|
audioEpisodeEnabled: require_dist.boolean().default(true),
|
|
32253
32281
|
/** Class must be heard continuously this long before the episode opens. */
|
|
32254
32282
|
audioEpisodeOpenHoldSec: require_dist.number().nonnegative().default(1.5),
|
|
@@ -34468,11 +34496,44 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
34468
34496
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
34469
34497
|
}
|
|
34470
34498
|
//#endregion
|
|
34499
|
+
//#region src/pipeline-analytics/pipeline/embedding-magnitude-gate.ts
|
|
34500
|
+
/**
|
|
34501
|
+
* `null` = accept. A verdict = refuse, carrying both numbers so the caller can
|
|
34502
|
+
* name them in a log.
|
|
34503
|
+
*
|
|
34504
|
+
* Accepts unconditionally when the bound is 0 (disabled) or when the magnitude
|
|
34505
|
+
* is absent / non-finite — an older runner that does not report it must keep
|
|
34506
|
+
* working exactly as before, never be silently gated to nothing. Pure.
|
|
34507
|
+
*/
|
|
34508
|
+
function embeddingMagnitudeVerdict(magnitude, bound) {
|
|
34509
|
+
if (!(bound > 0)) return null;
|
|
34510
|
+
if (magnitude === void 0 || !Number.isFinite(magnitude)) return null;
|
|
34511
|
+
if (magnitude <= bound) return null;
|
|
34512
|
+
return {
|
|
34513
|
+
magnitude,
|
|
34514
|
+
bound
|
|
34515
|
+
};
|
|
34516
|
+
}
|
|
34517
|
+
/**
|
|
34518
|
+
* Map a detection-frame face short side to the 0..1 `sizeScore` the
|
|
34519
|
+
* {@link ./best-detection-tracker.BestDetectionTracker} multiplies into its
|
|
34520
|
+
* effective score. Linear up to {@link FACE_SIZE_SCORE_REFERENCE_PX}, saturated
|
|
34521
|
+
* above it, 0 for a missing/degenerate measurement (so an unmeasurable face can
|
|
34522
|
+
* never outrank a measured one). Pure.
|
|
34523
|
+
*/
|
|
34524
|
+
function faceSizeScore(detectionFrameShortSidePx) {
|
|
34525
|
+
if (!Number.isFinite(detectionFrameShortSidePx) || detectionFrameShortSidePx <= 0) return 0;
|
|
34526
|
+
if (detectionFrameShortSidePx >= 32) return 1;
|
|
34527
|
+
return detectionFrameShortSidePx / 32;
|
|
34528
|
+
}
|
|
34529
|
+
//#endregion
|
|
34471
34530
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
34472
34531
|
/** Attribution of an identity resolved by the in-process ArcFace matcher. */
|
|
34473
34532
|
var FACE_RECOGNIZER_STEP_ID = "face-recognizer";
|
|
34474
34533
|
/** At most one "dropping imageless track" log per this interval, per recognizer. */
|
|
34475
34534
|
var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
|
|
34535
|
+
/** At most one 'embedding magnitude' sample line per device per this interval. */
|
|
34536
|
+
var FACE_MAGNITUDE_LOG_THROTTLE_MS = 6e4;
|
|
34476
34537
|
/**
|
|
34477
34538
|
* arcface model id stamped on a detail-plane face candidate when the gallery is
|
|
34478
34539
|
* empty (collect-only). The detail-subtree result carries no `embeddingModelId`
|
|
@@ -34487,6 +34548,8 @@ var FaceRecognizer = class {
|
|
|
34487
34548
|
names = /* @__PURE__ */ new Map();
|
|
34488
34549
|
aggregates = /* @__PURE__ */ new Map();
|
|
34489
34550
|
bestFace = /* @__PURE__ */ new Map();
|
|
34551
|
+
/** Per-device throttle clock for the magnitude sample line. */
|
|
34552
|
+
lastMagnitudeLogAt = /* @__PURE__ */ new Map();
|
|
34490
34553
|
/**
|
|
34491
34554
|
* Peak identity-MATCH cosine per track for the currently-assigned identity
|
|
34492
34555
|
* (NOT the detection confidence held in {@link bestFace}). Reset on an
|
|
@@ -34536,6 +34599,21 @@ var FaceRecognizer = class {
|
|
|
34536
34599
|
* input source moves from the per-frame plane to this per-track call.
|
|
34537
34600
|
*/
|
|
34538
34601
|
async ingestFaceDetail(input) {
|
|
34602
|
+
const verdict = embeddingMagnitudeVerdict(input.embeddingMagnitude, input.settings.maxEmbeddingMagnitude);
|
|
34603
|
+
if (verdict !== null) {
|
|
34604
|
+
this.deps.logger.info("face: dropping detail — raw embedding magnitude over bound", {
|
|
34605
|
+
tags: { deviceId: input.deviceId },
|
|
34606
|
+
meta: {
|
|
34607
|
+
trackId: input.trackId,
|
|
34608
|
+
magnitude: Number(verdict.magnitude.toFixed(3)),
|
|
34609
|
+
bound: verdict.bound,
|
|
34610
|
+
score: input.score,
|
|
34611
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: input.nativeFaceShortSidePx } : {}
|
|
34612
|
+
}
|
|
34613
|
+
});
|
|
34614
|
+
return;
|
|
34615
|
+
}
|
|
34616
|
+
this.logEmbeddingMagnitude(input);
|
|
34539
34617
|
const modelId = this.gallery[0]?.modelId ?? FALLBACK_FACE_MODEL_ID;
|
|
34540
34618
|
const candidate = {
|
|
34541
34619
|
trackId: input.trackId,
|
|
@@ -34562,6 +34640,30 @@ var FaceRecognizer = class {
|
|
|
34562
34640
|
...input.frameHandle !== void 0 ? { frameHandle: input.frameHandle } : {}
|
|
34563
34641
|
});
|
|
34564
34642
|
}
|
|
34643
|
+
/**
|
|
34644
|
+
* At most one magnitude line per device per
|
|
34645
|
+
* {@link FACE_MAGNITUDE_LOG_THROTTLE_MS}. This is the ONLY way the operator
|
|
34646
|
+
* can see where this cluster's usable and degenerate face populations sit, so
|
|
34647
|
+
* it must exist before `maxEmbeddingMagnitude` can honestly be turned on.
|
|
34648
|
+
*/
|
|
34649
|
+
logEmbeddingMagnitude(input) {
|
|
34650
|
+
const magnitude = input.embeddingMagnitude;
|
|
34651
|
+
if (magnitude === void 0 || !Number.isFinite(magnitude)) return;
|
|
34652
|
+
const now = Date.now();
|
|
34653
|
+
if (now - (this.lastMagnitudeLogAt.get(input.deviceId) ?? 0) < FACE_MAGNITUDE_LOG_THROTTLE_MS) return;
|
|
34654
|
+
this.lastMagnitudeLogAt.set(input.deviceId, now);
|
|
34655
|
+
this.deps.logger.info("face: embedding magnitude", {
|
|
34656
|
+
tags: { deviceId: input.deviceId },
|
|
34657
|
+
meta: {
|
|
34658
|
+
trackId: input.trackId,
|
|
34659
|
+
magnitude: Number(magnitude.toFixed(3)),
|
|
34660
|
+
bound: input.settings.maxEmbeddingMagnitude,
|
|
34661
|
+
score: input.score,
|
|
34662
|
+
detectionFrameShortSidePx: input.faceBbox !== void 0 ? Math.round(Math.min(input.faceBbox.w, input.faceBbox.h)) : 0,
|
|
34663
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: Math.round(input.nativeFaceShortSidePx) } : {}
|
|
34664
|
+
}
|
|
34665
|
+
});
|
|
34666
|
+
}
|
|
34565
34667
|
async processFrame(input) {
|
|
34566
34668
|
const { settings } = input;
|
|
34567
34669
|
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.nativeFaceShortSidePx ?? (t.faceBbox ? Math.min(t.faceBbox.w, t.faceBbox.h) : void 0) ?? Number.POSITIVE_INFINITY) >= settings.minFacePx);
|
|
@@ -34623,7 +34725,8 @@ var FaceRecognizer = class {
|
|
|
34623
34725
|
}
|
|
34624
34726
|
const recognizedIdentityId = state.assignedIdentityId ?? void 0;
|
|
34625
34727
|
const held = this.bestFace.get(c.trackId);
|
|
34626
|
-
const
|
|
34728
|
+
const detectionFrameShortSidePx = c.faceBbox !== void 0 ? Math.min(c.faceBbox.w, c.faceBbox.h) : 0;
|
|
34729
|
+
const isNewBest = this.bestTracker.observe(c.trackId, c.confidence, input.timestamp, void 0, void 0, void 0, faceSizeScore(detectionFrameShortSidePx));
|
|
34627
34730
|
const needsCrop = held !== void 0 && held.crop === void 0;
|
|
34628
34731
|
if (isNewBest || needsCrop) {
|
|
34629
34732
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
@@ -34883,14 +34986,38 @@ var FaceSettingsSchema = require_dist.object({
|
|
|
34883
34986
|
* stripped from the per-device schema).
|
|
34884
34987
|
*/
|
|
34885
34988
|
enabled: require_dist.boolean().default(true),
|
|
34886
|
-
/**
|
|
34887
|
-
*
|
|
34888
|
-
*
|
|
34889
|
-
*
|
|
34890
|
-
|
|
34891
|
-
|
|
34892
|
-
*
|
|
34893
|
-
|
|
34989
|
+
/**
|
|
34990
|
+
* Cosine similarity (on L2-normalized arcface vectors) required to match.
|
|
34991
|
+
*
|
|
34992
|
+
* History: 0.55→0.62 on 2026-07-23, then back to 0.55 on 2026-08-18 once the
|
|
34993
|
+
* dead `assignUniquePerFrame` margin and the poisoned gallery were fixed —
|
|
34994
|
+
* with both repaired, genuine probes scored median 0.38 / p90 0.51, so 0.62
|
|
34995
|
+
* left almost nothing recognisable. That retune lived only as a RUNTIME
|
|
34996
|
+
* override until 2026-08-19; it is the schema default now, because a default
|
|
34997
|
+
* nobody runs is a decision nobody made.
|
|
34998
|
+
*
|
|
34999
|
+
* ⚠ NOT the end state. 0.55 is the least-bad value for the CURRENT crop
|
|
35000
|
+
* quality, where most of what reaches ArcFace is not a correctly aligned
|
|
35001
|
+
* face (measured 2026-08-19 over 60 live crops pulled from the hub: 20%
|
|
35002
|
+
* aligned, 18% marginal, 62% misaligned or containing no detectable face).
|
|
35003
|
+
* On correctly aligned crops the SAME models separate at a far lower
|
|
35004
|
+
* operating point — 300 LFW pairs gave genuine median 0.605 / impostor p90
|
|
35005
|
+
* 0.175, best accuracy 98.3% at threshold 0.28.
|
|
35006
|
+
*
|
|
35007
|
+
* Recalibration plan, in this order:
|
|
35008
|
+
* 1. land the landmark-precision gate (the alignment fix),
|
|
35009
|
+
* 2. re-enrol the gallery from crops produced AFTER it,
|
|
35010
|
+
* 3. re-measure genuine/impostor over a week of post-fix traffic,
|
|
35011
|
+
* 4. expect to lower this toward ~0.30, and `margin` with it.
|
|
35012
|
+
* Lowering it BEFORE step 1 would be actively harmful: a misaligned crop
|
|
35013
|
+
* scores ~0.9 against everything, so the degenerate population sits ABOVE
|
|
35014
|
+
* any threshold you could pick — the knob cannot reach it.
|
|
35015
|
+
*/
|
|
35016
|
+
similarityThreshold: require_dist.number().min(0).max(1).default(.55),
|
|
35017
|
+
/** Reject ambiguous matches: require best − secondBest ≥ margin. 0.10→0.15
|
|
35018
|
+
* (2026-07-23), then →0.12 (2026-08-18) alongside the threshold retune —
|
|
35019
|
+
* same story and the same recalibration plan as {@link similarityThreshold}. */
|
|
35020
|
+
margin: require_dist.number().min(0).max(1).default(.12),
|
|
34894
35021
|
/** Minimum face-detection confidence for a face to be considered. */
|
|
34895
35022
|
minFaceConfidence: require_dist.number().min(0).max(1).default(.5),
|
|
34896
35023
|
/**
|
|
@@ -34934,6 +35061,33 @@ var FaceSettingsSchema = require_dist.object({
|
|
|
34934
35061
|
* 1 (0 confirmations would assign on a single noisy frame — nonsensical;
|
|
34935
35062
|
* such a value falls back to the default). */
|
|
34936
35063
|
confirmFrames: require_dist.number().int().min(1).default(3),
|
|
35064
|
+
/**
|
|
35065
|
+
* Upper bound on the RAW ArcFace feature magnitude a face may have and still
|
|
35066
|
+
* be embedded into the buffer / matched / enrolled. `0` (the default) DISABLES
|
|
35067
|
+
* the gate.
|
|
35068
|
+
*
|
|
35069
|
+
* This exists because a degenerate crop does not score LOW, it scores HIGH:
|
|
35070
|
+
* measured on this cluster 2026-08-18/19, 28.8% of all pairs of DIFFERENT
|
|
35071
|
+
* faces exceeded cosine 0.90 and the worst 40% of the face population had
|
|
35072
|
+
* collapsed onto effectively one vector (mean pairwise cosine 0.976). No
|
|
35073
|
+
* similarity threshold can reach that — it sits above every threshold — so the
|
|
35074
|
+
* only defence is to refuse the embedding before it is stored or compared.
|
|
35075
|
+
*
|
|
35076
|
+
* Direction: for this model the DEGENERATE crops carry the LARGER norm (the
|
|
35077
|
+
* inverse of the MagFace convention), hence a maximum. Over 140 LFW faces
|
|
35078
|
+
* through this exact model and preprocessing, usable crops sat at ≈4.4 (p95
|
|
35079
|
+
* 5.25) regardless of size, grey level or blur, while information-free crops
|
|
35080
|
+
* jumped to 10–16; a bound of 6.0 kept 97.6% of usable crops and rejected
|
|
35081
|
+
* 93.2% of degenerate ones.
|
|
35082
|
+
*
|
|
35083
|
+
* Default 0 ON PURPOSE. Those numbers are LFW's, not this cluster's, and the
|
|
35084
|
+
* magnitude has never been observable here — `result-assembler` discarded it
|
|
35085
|
+
* one line after computing it. The field and its logging ship first; set a
|
|
35086
|
+
* real bound once a week of live `face: embedding magnitude` lines says where
|
|
35087
|
+
* this cluster's populations actually sit. Enabling a gate against a number
|
|
35088
|
+
* nobody has measured in production is how recognition silently stops.
|
|
35089
|
+
*/
|
|
35090
|
+
maxEmbeddingMagnitude: require_dist.number().min(0).default(0),
|
|
34937
35091
|
/** Max buffered faces kept per device. */
|
|
34938
35092
|
bufferMaxPerDevice: require_dist.number().int().min(0).default(50)
|
|
34939
35093
|
});
|
|
@@ -34952,6 +35106,7 @@ function resolveFaceSettings(raw) {
|
|
|
34952
35106
|
recognitionMinFacePx: pick("recognitionMinFacePx"),
|
|
34953
35107
|
suggestionMinCosine: pick("suggestionMinCosine"),
|
|
34954
35108
|
minIdentitySamples: pick("minIdentitySamples"),
|
|
35109
|
+
maxEmbeddingMagnitude: pick("maxEmbeddingMagnitude"),
|
|
34955
35110
|
confirmFrames: pick("confirmFrames"),
|
|
34956
35111
|
bufferMaxPerDevice: pick("bufferMaxPerDevice")
|
|
34957
35112
|
};
|
|
@@ -39423,6 +39578,147 @@ async function applyTrackFlags(deps, input) {
|
|
|
39423
39578
|
};
|
|
39424
39579
|
}
|
|
39425
39580
|
//#endregion
|
|
39581
|
+
//#region src/pipeline-analytics/pipeline/plate-vote.ts
|
|
39582
|
+
/**
|
|
39583
|
+
* Per-character VOTE over a track's plate reads.
|
|
39584
|
+
*
|
|
39585
|
+
* A track produces up to `plate-ocr`'s `cadence.maxPerTrack` reads of the SAME
|
|
39586
|
+
* physical plate, from different frames. The recognizer used to keep the single
|
|
39587
|
+
* read with the highest OCR score (`holdBest`, argmax) — and OCR score does not
|
|
39588
|
+
* correlate with correctness. In the 2026-08 audit the top-scoring read of a
|
|
39589
|
+
* track was routinely the wrong one while two lower-scoring reads agreed on the
|
|
39590
|
+
* right glyph: score ranks how confident the recognizer was about the pixels it
|
|
39591
|
+
* saw, not how much of the plate those pixels were.
|
|
39592
|
+
*
|
|
39593
|
+
* So the reads vote. Each read is normalized (upper, alnum only), aligned to a
|
|
39594
|
+
* reference read, and contributes its OCR score to whichever glyph it saw at
|
|
39595
|
+
* each reference position; the winner per position is the output. Two reads
|
|
39596
|
+
* that agree beat one confident read that disagrees, which is the whole point.
|
|
39597
|
+
*
|
|
39598
|
+
* ALIGNMENT — reads differ in length (4–9 glyphs live: a dropped leading
|
|
39599
|
+
* district letter, a hallucinated trailing glyph). A positional vote on raw
|
|
39600
|
+
* indices would let one missing character shift every later column and destroy
|
|
39601
|
+
* the agreement it was supposed to measure. Each read is therefore aligned to
|
|
39602
|
+
* the reference by Levenshtein traceback: a match or substitution votes at the
|
|
39603
|
+
* reference position it aligned to, an insertion votes nowhere, and a deletion
|
|
39604
|
+
* simply abstains at that position.
|
|
39605
|
+
*
|
|
39606
|
+
* REFERENCE — the length carrying the most SCORE MASS wins (a hallucinated
|
|
39607
|
+
* 9-glyph read does not get to set the output length just by being longest);
|
|
39608
|
+
* ties inside that length go to the highest-scoring read. With ≤5 reads this is
|
|
39609
|
+
* the honest cheap heuristic: there is no third source of truth to consult.
|
|
39610
|
+
*/
|
|
39611
|
+
/**
|
|
39612
|
+
* Align `candidate` to `reference` by Levenshtein traceback. Returns one entry
|
|
39613
|
+
* per REFERENCE position: the candidate glyph that aligned there, or `null`
|
|
39614
|
+
* when the candidate had nothing at that position (a deletion). Candidate
|
|
39615
|
+
* insertions are dropped — they have no reference column to vote in.
|
|
39616
|
+
*/
|
|
39617
|
+
function alignToReference(reference, candidate) {
|
|
39618
|
+
const n = reference.length;
|
|
39619
|
+
const m = candidate.length;
|
|
39620
|
+
const dist = [];
|
|
39621
|
+
for (let i = 0; i <= n; i++) {
|
|
39622
|
+
const row = new Array(m + 1).fill(0);
|
|
39623
|
+
row[0] = i;
|
|
39624
|
+
dist.push(row);
|
|
39625
|
+
}
|
|
39626
|
+
const first = dist[0];
|
|
39627
|
+
if (first === void 0) return [];
|
|
39628
|
+
for (let j = 0; j <= m; j++) first[j] = j;
|
|
39629
|
+
for (let i = 1; i <= n; i++) {
|
|
39630
|
+
const prev = dist[i - 1];
|
|
39631
|
+
const cur = dist[i];
|
|
39632
|
+
if (prev === void 0 || cur === void 0) continue;
|
|
39633
|
+
for (let j = 1; j <= m; j++) {
|
|
39634
|
+
const cost = reference[i - 1] === candidate[j - 1] ? 0 : 1;
|
|
39635
|
+
cur[j] = Math.min((prev[j] ?? 0) + 1, (cur[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
39636
|
+
}
|
|
39637
|
+
}
|
|
39638
|
+
const aligned = new Array(n).fill(null);
|
|
39639
|
+
let i = n;
|
|
39640
|
+
let j = m;
|
|
39641
|
+
while (i > 0 && j > 0) {
|
|
39642
|
+
const cur = dist[i];
|
|
39643
|
+
const prev = dist[i - 1];
|
|
39644
|
+
if (cur === void 0 || prev === void 0) break;
|
|
39645
|
+
const cost = reference[i - 1] === candidate[j - 1] ? 0 : 1;
|
|
39646
|
+
if (cur[j] === (prev[j - 1] ?? 0) + cost) {
|
|
39647
|
+
aligned[i - 1] = candidate[j - 1] ?? null;
|
|
39648
|
+
i--;
|
|
39649
|
+
j--;
|
|
39650
|
+
} else if (cur[j] === (prev[j] ?? 0) + 1) i--;
|
|
39651
|
+
else j--;
|
|
39652
|
+
}
|
|
39653
|
+
return aligned;
|
|
39654
|
+
}
|
|
39655
|
+
/** The read whose LENGTH carries the most score mass; ties → highest score. */
|
|
39656
|
+
function pickReference(reads) {
|
|
39657
|
+
const massByLength = /* @__PURE__ */ new Map();
|
|
39658
|
+
for (const r of reads) massByLength.set(r.text.length, (massByLength.get(r.text.length) ?? 0) + r.score);
|
|
39659
|
+
let bestLength = 0;
|
|
39660
|
+
let bestMass = -1;
|
|
39661
|
+
for (const [length, mass] of massByLength) if (mass > bestMass || mass === bestMass && length < bestLength) {
|
|
39662
|
+
bestMass = mass;
|
|
39663
|
+
bestLength = length;
|
|
39664
|
+
}
|
|
39665
|
+
let reference = "";
|
|
39666
|
+
let referenceScore = -1;
|
|
39667
|
+
for (const r of reads) if (r.text.length === bestLength && r.score > referenceScore) {
|
|
39668
|
+
referenceScore = r.score;
|
|
39669
|
+
reference = r.text;
|
|
39670
|
+
}
|
|
39671
|
+
return reference;
|
|
39672
|
+
}
|
|
39673
|
+
/**
|
|
39674
|
+
* Confidence-weighted per-character vote over a track's plate reads. Returns
|
|
39675
|
+
* the normalized winning text (`''` when there is nothing to vote on).
|
|
39676
|
+
*/
|
|
39677
|
+
function votePlateText(reads) {
|
|
39678
|
+
const normalized = [];
|
|
39679
|
+
for (const r of reads) {
|
|
39680
|
+
const text = normalizePlate(r.text);
|
|
39681
|
+
if (text.length > 0) normalized.push({
|
|
39682
|
+
text,
|
|
39683
|
+
score: r.score
|
|
39684
|
+
});
|
|
39685
|
+
}
|
|
39686
|
+
if (normalized.length === 0) return "";
|
|
39687
|
+
const only = normalized[0];
|
|
39688
|
+
if (normalized.length === 1 && only !== void 0) return only.text;
|
|
39689
|
+
const reference = pickReference(normalized);
|
|
39690
|
+
if (reference.length === 0) return "";
|
|
39691
|
+
const columns = [];
|
|
39692
|
+
for (let i = 0; i < reference.length; i++) columns.push(/* @__PURE__ */ new Map());
|
|
39693
|
+
for (const r of normalized) {
|
|
39694
|
+
const aligned = r.text === reference ? [...r.text] : alignToReference(reference, r.text);
|
|
39695
|
+
for (let i = 0; i < reference.length; i++) {
|
|
39696
|
+
const glyph = aligned[i];
|
|
39697
|
+
if (glyph === null || glyph === void 0) continue;
|
|
39698
|
+
const column = columns[i];
|
|
39699
|
+
if (column === void 0) continue;
|
|
39700
|
+
column.set(glyph, (column.get(glyph) ?? 0) + r.score);
|
|
39701
|
+
}
|
|
39702
|
+
}
|
|
39703
|
+
let out = "";
|
|
39704
|
+
for (let i = 0; i < reference.length; i++) {
|
|
39705
|
+
const column = columns[i];
|
|
39706
|
+
const referenceGlyph = reference[i] ?? "";
|
|
39707
|
+
if (column === void 0 || column.size === 0) {
|
|
39708
|
+
out += referenceGlyph;
|
|
39709
|
+
continue;
|
|
39710
|
+
}
|
|
39711
|
+
let bestGlyph = referenceGlyph;
|
|
39712
|
+
let bestMass = column.get(referenceGlyph) ?? -1;
|
|
39713
|
+
for (const [glyph, mass] of column) if (mass > bestMass) {
|
|
39714
|
+
bestMass = mass;
|
|
39715
|
+
bestGlyph = glyph;
|
|
39716
|
+
}
|
|
39717
|
+
out += bestGlyph;
|
|
39718
|
+
}
|
|
39719
|
+
return out;
|
|
39720
|
+
}
|
|
39721
|
+
//#endregion
|
|
39426
39722
|
//#region src/pipeline-analytics/pipeline/plate-matcher.ts
|
|
39427
39723
|
/**
|
|
39428
39724
|
* plate-matcher — name a plate read against the vehicle gallery by text vicinity.
|
|
@@ -39471,11 +39767,11 @@ function matchPlateText(probe, gallery, opts) {
|
|
|
39471
39767
|
//#endregion
|
|
39472
39768
|
//#region src/pipeline-analytics/plate-recognizer.ts
|
|
39473
39769
|
/**
|
|
39474
|
-
* PlateRecognizer —
|
|
39475
|
-
*
|
|
39476
|
-
*
|
|
39477
|
-
*
|
|
39478
|
-
*
|
|
39770
|
+
* PlateRecognizer — collects EVERY license-plate OCR read a vehicle track
|
|
39771
|
+
* produces and stores their per-character VOTE (`plate-vote.ts`) at close, plus
|
|
39772
|
+
* the max-score read's crop for the gallery tile. Unlike faces there is NO
|
|
39773
|
+
* gallery matching: the OCR text IS the label and already rides on the event via
|
|
39774
|
+
* the detection pipeline. Mirrors FaceRecognizer's hold→onTrackEnd lifecycle.
|
|
39479
39775
|
*/
|
|
39480
39776
|
var PlateRecognizer = class {
|
|
39481
39777
|
deps;
|
|
@@ -39538,7 +39834,7 @@ var PlateRecognizer = class {
|
|
|
39538
39834
|
const minConfidence = input.minConfidence ?? 0;
|
|
39539
39835
|
const candidates = input.tracked.filter((t) => typeof t.plateText === "string" && t.plateText.length > 0 && typeof t.plateScore === "number" && t.plateScore >= minConfidence && t.plateBbox !== void 0);
|
|
39540
39836
|
if (candidates.length === 0) return;
|
|
39541
|
-
for (const c of candidates) await this.
|
|
39837
|
+
for (const c of candidates) await this.observeRead({
|
|
39542
39838
|
deviceId: input.deviceId,
|
|
39543
39839
|
trackId: c.trackId,
|
|
39544
39840
|
text: c.plateText,
|
|
@@ -39561,15 +39857,50 @@ var PlateRecognizer = class {
|
|
|
39561
39857
|
* gallery stays empty (0 plateCrop) — the observed live gap.
|
|
39562
39858
|
*/
|
|
39563
39859
|
async observePlateRead(input) {
|
|
39564
|
-
|
|
39565
|
-
|
|
39566
|
-
|
|
39860
|
+
const minConfidence = input.minConfidence ?? 0;
|
|
39861
|
+
const reason = !isPlausiblePlateRead(input.text, input.score) ? "implausible" : input.score < minConfidence ? "below-min-confidence" : null;
|
|
39862
|
+
if (reason !== null) {
|
|
39863
|
+
this.deps.logger.info("plate: read discarded", {
|
|
39864
|
+
tags: {
|
|
39865
|
+
deviceId: input.deviceId,
|
|
39866
|
+
trackId: input.trackId
|
|
39867
|
+
},
|
|
39868
|
+
meta: {
|
|
39869
|
+
text: input.text,
|
|
39870
|
+
score: input.score,
|
|
39871
|
+
minConfidence,
|
|
39872
|
+
reason
|
|
39873
|
+
}
|
|
39874
|
+
});
|
|
39875
|
+
return;
|
|
39876
|
+
}
|
|
39877
|
+
await this.observeRead(input);
|
|
39567
39878
|
}
|
|
39568
|
-
/**
|
|
39569
|
-
*
|
|
39570
|
-
|
|
39879
|
+
/**
|
|
39880
|
+
* Record one plate read for a track (shared by the per-frame + detail-plane
|
|
39881
|
+
* paths).
|
|
39882
|
+
*
|
|
39883
|
+
* EVERY read is kept for the close-time vote — a track's reads disagree, and
|
|
39884
|
+
* the disagreement is the signal. The crop + geometry still track the
|
|
39885
|
+
* MAX-SCORE read: the vote decides the text, but the picture the operator
|
|
39886
|
+
* reviews should be the sharpest frame the track produced.
|
|
39887
|
+
*
|
|
39888
|
+
* The read list is capped at {@link MAX_VOTING_READS} in ARRIVAL order, not
|
|
39889
|
+
* by score: evicting the lowest-scoring read would re-introduce the very
|
|
39890
|
+
* ranking this vote exists to stop trusting.
|
|
39891
|
+
*/
|
|
39892
|
+
async observeRead(input) {
|
|
39571
39893
|
const held = this.bestPlate.get(input.trackId);
|
|
39572
|
-
|
|
39894
|
+
const vote = {
|
|
39895
|
+
text: input.text,
|
|
39896
|
+
score: input.score
|
|
39897
|
+
};
|
|
39898
|
+
if (held !== void 0 && input.score <= held.score) {
|
|
39899
|
+
if (held.reads.length < 5) held.reads.push(vote);
|
|
39900
|
+
return;
|
|
39901
|
+
}
|
|
39902
|
+
const reads = held?.reads ?? [];
|
|
39903
|
+
if (reads.length < 5) reads.push(vote);
|
|
39573
39904
|
let crop;
|
|
39574
39905
|
if (input.frameHandle !== void 0) try {
|
|
39575
39906
|
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId, input.deviceId) ?? void 0;
|
|
@@ -39585,6 +39916,7 @@ var PlateRecognizer = class {
|
|
|
39585
39916
|
this.bestPlate.set(input.trackId, {
|
|
39586
39917
|
text: input.text,
|
|
39587
39918
|
score: input.score,
|
|
39919
|
+
reads,
|
|
39588
39920
|
bbox: input.bbox,
|
|
39589
39921
|
timestamp: input.timestamp,
|
|
39590
39922
|
frameWidth: input.frameWidth,
|
|
@@ -39661,14 +39993,16 @@ var PlateRecognizer = class {
|
|
|
39661
39993
|
}
|
|
39662
39994
|
});
|
|
39663
39995
|
}
|
|
39664
|
-
const
|
|
39996
|
+
const voted = votePlateText(held.reads);
|
|
39997
|
+
const text = voted.length > 0 && isPlausiblePlateRead(voted, held.score) ? voted : normalizePlate(held.text);
|
|
39998
|
+
const match = this.matchVehicle(text, held.score);
|
|
39665
39999
|
try {
|
|
39666
40000
|
await this.deps.plateStore.insert({
|
|
39667
40001
|
id: plateId,
|
|
39668
40002
|
deviceId,
|
|
39669
40003
|
trackId,
|
|
39670
40004
|
timestamp: held.timestamp,
|
|
39671
|
-
text
|
|
40005
|
+
text,
|
|
39672
40006
|
score: held.score,
|
|
39673
40007
|
...mediaKey !== void 0 ? { mediaKey } : {},
|
|
39674
40008
|
corrected: false,
|
|
@@ -39689,7 +40023,10 @@ var PlateRecognizer = class {
|
|
|
39689
40023
|
},
|
|
39690
40024
|
meta: {
|
|
39691
40025
|
plateId,
|
|
39692
|
-
text
|
|
40026
|
+
text,
|
|
40027
|
+
bestRead: normalizePlate(held.text),
|
|
40028
|
+
reads: held.reads.map((r) => `${normalizePlate(r.text)}@${r.score.toFixed(2)}`),
|
|
40029
|
+
votedFromReads: held.reads.length,
|
|
39693
40030
|
score: held.score,
|
|
39694
40031
|
hasCrop: mediaKey !== void 0,
|
|
39695
40032
|
recognizedVehicleId: match?.vehicleId ?? null
|
|
@@ -50042,7 +50379,7 @@ function buildGlobalSettingsSchema() {
|
|
|
50042
50379
|
type: "boolean",
|
|
50043
50380
|
key: "audioEpisodeEnabled",
|
|
50044
50381
|
label: "Create audio episodes",
|
|
50045
|
-
description: "
|
|
50382
|
+
description: "Off switches off the GROUPING only: classified audio still persists an event and still notifies, but no timeline track is opened and a long sound is no longer collapsed into one episode (one event per class change, then one every 5 s while it lasts).",
|
|
50046
50383
|
default: AUDIO_DETECTION_DEFAULTS.audioEpisodeEnabled
|
|
50047
50384
|
},
|
|
50048
50385
|
{
|
|
@@ -50168,6 +50505,15 @@ function buildGlobalSettingsSchema() {
|
|
|
50168
50505
|
step: 1,
|
|
50169
50506
|
default: FACE_DEFAULTS.confirmFrames
|
|
50170
50507
|
},
|
|
50508
|
+
{
|
|
50509
|
+
type: "number",
|
|
50510
|
+
key: "maxEmbeddingMagnitude",
|
|
50511
|
+
label: "Max embedding magnitude (0 = off)",
|
|
50512
|
+
description: "Refuse a face whose RAW ArcFace feature magnitude exceeds this, before it is buffered, matched or enrolled. A degenerate crop does not score LOW against the gallery — it scores HIGH against everything — so no similarity threshold can catch it; the raw magnitude can. On this model a usable face sits around 4.4 whatever its size, grey level or blur, while an information-free crop jumps to 10–16. Leave at 0 until the \"face: embedding magnitude\" log lines show where YOUR cameras sit; a bound guessed from someone else's data silently stops recognising everybody.",
|
|
50513
|
+
min: 0,
|
|
50514
|
+
step: .5,
|
|
50515
|
+
default: FACE_DEFAULTS.maxEmbeddingMagnitude
|
|
50516
|
+
},
|
|
50171
50517
|
{
|
|
50172
50518
|
type: "number",
|
|
50173
50519
|
key: "bufferMaxPerDevice",
|
|
@@ -51548,6 +51894,16 @@ var OVERLAY_SYNTHESIS_WARN_THROTTLE_MS = 6e4;
|
|
|
51548
51894
|
var AUDIO_EPISODE_VISUAL_QUIET_MS = 2e3;
|
|
51549
51895
|
/** Persist lastSeen on an open synthetic audio track at most this often. */
|
|
51550
51896
|
var AUDIO_EPISODE_PERSIST_MS = 2e3;
|
|
51897
|
+
/**
|
|
51898
|
+
* Coalescing window for classified audio inserts when episodes are OFF.
|
|
51899
|
+
*
|
|
51900
|
+
* With `audioEpisodeEnabled: false` there is no hysteresis to collapse a sound
|
|
51901
|
+
* into one row, but the classifier still fires at ~30 Hz — so the pre-episode
|
|
51902
|
+
* rule applies again: while the SAME class persists, one row per heartbeat;
|
|
51903
|
+
* a class change inserts immediately. The knob turns off the grouping, never
|
|
51904
|
+
* the persistence (D62).
|
|
51905
|
+
*/
|
|
51906
|
+
var AUDIO_EVENT_HEARTBEAT_MS = 5e3;
|
|
51551
51907
|
var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
51552
51908
|
/**
|
|
51553
51909
|
* Stored media kinds that carry NO drawn bounding box, in fallback preference
|
|
@@ -55403,6 +55759,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55403
55759
|
frameHeight: frame.frameHeight,
|
|
55404
55760
|
score: detail.score,
|
|
55405
55761
|
embedding: decodeEmbeddingBase64(detail.embedding),
|
|
55762
|
+
...detail.embeddingMagnitude !== void 0 ? { embeddingMagnitude: detail.embeddingMagnitude } : {},
|
|
55406
55763
|
parentBbox: { ...frame.bbox },
|
|
55407
55764
|
...detail.bbox !== void 0 ? { faceBbox: { ...detail.bbox } } : {},
|
|
55408
55765
|
...detail.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: detail.nativeFaceShortSidePx } : {},
|
|
@@ -55870,7 +56227,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55870
56227
|
return;
|
|
55871
56228
|
}
|
|
55872
56229
|
}
|
|
55873
|
-
|
|
56230
|
+
/**
|
|
56231
|
+
* The ONE writer for a classified audio observation: the durable row, the
|
|
56232
|
+
* Notification Center hook fired in the same persist moment (D-2), the
|
|
56233
|
+
* track-level `audioLabels` stamp, and the telemetry event.
|
|
56234
|
+
*
|
|
56235
|
+
* Shared by both routes on purpose. `audioEpisodeEnabled` selects WHEN this
|
|
56236
|
+
* runs (once per episode, or once per heartbeat), never WHETHER — a knob
|
|
56237
|
+
* that names the grouping must not be able to silence the classified path
|
|
56238
|
+
* (D62). This body was inlined in `openAudioEpisode` and the knob-off camera
|
|
56239
|
+
* therefore persisted nothing at all.
|
|
56240
|
+
*/
|
|
56241
|
+
async persistClassifiedAudio(input) {
|
|
55874
56242
|
const ev = {
|
|
55875
56243
|
id: (0, node_crypto.randomUUID)(),
|
|
55876
56244
|
deviceId: input.deviceId,
|
|
@@ -55879,18 +56247,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55879
56247
|
rms: input.rms,
|
|
55880
56248
|
dbfs: input.dbfs,
|
|
55881
56249
|
classification: {
|
|
55882
|
-
className:
|
|
56250
|
+
className: input.className,
|
|
55883
56251
|
...input.originalClass !== void 0 ? { originalClass: input.originalClass } : {},
|
|
55884
56252
|
score: input.score
|
|
55885
56253
|
}
|
|
55886
56254
|
};
|
|
55887
56255
|
this.lastAudioInsertByDevice.set(input.deviceId, {
|
|
55888
|
-
className:
|
|
56256
|
+
className: input.className,
|
|
55889
56257
|
atMs: input.timestamp
|
|
55890
56258
|
});
|
|
55891
56259
|
await this.eventStore.insertAudio(ev);
|
|
55892
56260
|
this.notificationCenter?.onAudioEventPersisted(ev);
|
|
55893
|
-
this.trackStore?.addAudioLabelEpisode(input.deviceId,
|
|
56261
|
+
this.trackStore?.addAudioLabelEpisode(input.deviceId, input.className, input.score, input.timestamp);
|
|
55894
56262
|
this.ctx.eventBus.emit({
|
|
55895
56263
|
id: `pa-${ev.id}`,
|
|
55896
56264
|
timestamp: new Date(ev.timestamp),
|
|
@@ -55907,6 +56275,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55907
56275
|
timestamp: ev.timestamp
|
|
55908
56276
|
}
|
|
55909
56277
|
});
|
|
56278
|
+
}
|
|
56279
|
+
/**
|
|
56280
|
+
* Classified audio with episodes OFF: persist on class change or heartbeat,
|
|
56281
|
+
* and open NO synthetic track. This is the whole of what the knob switches —
|
|
56282
|
+
* the timeline track and the episode coalescing — and it is what the form
|
|
56283
|
+
* promises.
|
|
56284
|
+
*/
|
|
56285
|
+
async persistClassifiedAudioUngrouped(input) {
|
|
56286
|
+
const last = this.lastAudioInsertByDevice.get(input.deviceId);
|
|
56287
|
+
const sameClass = last?.className === input.className;
|
|
56288
|
+
const heartbeatDue = last === void 0 || input.timestamp - last.atMs >= AUDIO_EVENT_HEARTBEAT_MS;
|
|
56289
|
+
if (sameClass && !heartbeatDue) return;
|
|
56290
|
+
await this.persistClassifiedAudio(input);
|
|
56291
|
+
}
|
|
56292
|
+
async openAudioEpisode(input, ref) {
|
|
56293
|
+
await this.persistClassifiedAudio({
|
|
56294
|
+
...input,
|
|
56295
|
+
className: ref.className
|
|
56296
|
+
});
|
|
55910
56297
|
if (this.hasLiveVisualTrack(input.deviceId, input.timestamp)) return;
|
|
55911
56298
|
const materializer = this.syntheticTracks;
|
|
55912
56299
|
if (materializer === null) return;
|
|
@@ -56018,19 +56405,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
56018
56405
|
});
|
|
56019
56406
|
return;
|
|
56020
56407
|
}
|
|
56021
|
-
|
|
56408
|
+
const classified = {
|
|
56022
56409
|
deviceId,
|
|
56023
56410
|
timestamp,
|
|
56024
56411
|
className: route.className,
|
|
56025
56412
|
score: topClassification.score,
|
|
56026
|
-
originalClass: topClassification.debug
|
|
56413
|
+
...topClassification.debug?.originalClass !== void 0 ? { originalClass: topClassification.debug.originalClass } : {},
|
|
56027
56414
|
rms: level?.rms ?? 0,
|
|
56028
|
-
dbfs: level?.dbfs ?? 0
|
|
56415
|
+
dbfs: level?.dbfs ?? 0
|
|
56416
|
+
};
|
|
56417
|
+
if (!settings.audioEpisodeEnabled) {
|
|
56418
|
+
await this.persistClassifiedAudioUngrouped(classified);
|
|
56419
|
+
return;
|
|
56420
|
+
}
|
|
56421
|
+
await this.driveAudioEpisode({
|
|
56422
|
+
...classified,
|
|
56029
56423
|
knobs: this.audioEpisodeKnobs(settings)
|
|
56030
56424
|
});
|
|
56031
56425
|
return;
|
|
56032
56426
|
}
|
|
56033
|
-
await this.driveAudioEpisode({
|
|
56427
|
+
if (settings.audioEpisodeEnabled) await this.driveAudioEpisode({
|
|
56034
56428
|
deviceId,
|
|
56035
56429
|
timestamp,
|
|
56036
56430
|
className: null,
|