@camstack/addon-post-analysis 1.2.101 → 1.2.103
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 +676 -62
- package/dist/pipeline-analytics/index.mjs +676 -62
- 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");
|
|
@@ -8302,6 +8302,10 @@ var en_default = {
|
|
|
8302
8302
|
"one": "plate",
|
|
8303
8303
|
"other": "plates"
|
|
8304
8304
|
},
|
|
8305
|
+
"class.audio": {
|
|
8306
|
+
"one": "sound",
|
|
8307
|
+
"other": "sounds"
|
|
8308
|
+
},
|
|
8305
8309
|
"class.other": "{{name}}",
|
|
8306
8310
|
"group.others.person": {
|
|
8307
8311
|
"one": "{{count}} other person",
|
|
@@ -8359,8 +8363,11 @@ var en_default = {
|
|
|
8359
8363
|
"one": "{{count}} addon update available",
|
|
8360
8364
|
"other": "{{count}} addon updates available"
|
|
8361
8365
|
},
|
|
8366
|
+
"system.addon-update-available.title.onNode": {
|
|
8367
|
+
"one": "{{count}} addon update available on {{nodeId}}",
|
|
8368
|
+
"other": "{{count}} addon updates available on {{nodeId}}"
|
|
8369
|
+
},
|
|
8362
8370
|
"system.addon-update-available.body": "{{list}}",
|
|
8363
|
-
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
8364
8371
|
"system.server-update-available.title": {
|
|
8365
8372
|
"one": "Server update available on {{count}} node",
|
|
8366
8373
|
"other": "Server update available on {{count}} nodes"
|
|
@@ -8375,6 +8382,7 @@ var en_default = {
|
|
|
8375
8382
|
"system.addon-updated.body": "{{list}}",
|
|
8376
8383
|
"system.server-updated.title": "Server updated",
|
|
8377
8384
|
"system.server-updated.body": "{{packageName}} on {{nodeId}} is now {{toVersion}} (was {{fromVersion}})",
|
|
8385
|
+
"system.server-updated.body.toOnly": "{{packageName}} on {{nodeId}} is now {{toVersion}}",
|
|
8378
8386
|
"system.server-updated.body.plain": "{{packageName}} on {{nodeId}} was updated",
|
|
8379
8387
|
"system.export-completed.title": "Export ready",
|
|
8380
8388
|
"system.export-completed.body": "The footage export is ready",
|
|
@@ -8516,6 +8524,10 @@ var it_default = {
|
|
|
8516
8524
|
"one": "targa",
|
|
8517
8525
|
"other": "targhe"
|
|
8518
8526
|
},
|
|
8527
|
+
"class.audio": {
|
|
8528
|
+
"one": "suono",
|
|
8529
|
+
"other": "suoni"
|
|
8530
|
+
},
|
|
8519
8531
|
"class.other": "{{name}}",
|
|
8520
8532
|
"group.others.person": {
|
|
8521
8533
|
"one": "{{count}} altra persona",
|
|
@@ -8573,8 +8585,11 @@ var it_default = {
|
|
|
8573
8585
|
"one": "{{count}} aggiornamento addon disponibile",
|
|
8574
8586
|
"other": "{{count}} aggiornamenti addon disponibili"
|
|
8575
8587
|
},
|
|
8588
|
+
"system.addon-update-available.title.onNode": {
|
|
8589
|
+
"one": "{{count}} aggiornamento addon disponibile su {{nodeId}}",
|
|
8590
|
+
"other": "{{count}} aggiornamenti addon disponibili su {{nodeId}}"
|
|
8591
|
+
},
|
|
8576
8592
|
"system.addon-update-available.body": "{{list}}",
|
|
8577
|
-
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
8578
8593
|
"system.server-update-available.title": {
|
|
8579
8594
|
"one": "Aggiornamento server disponibile su {{count}} nodo",
|
|
8580
8595
|
"other": "Aggiornamento server disponibile su {{count}} nodi"
|
|
@@ -8589,6 +8604,7 @@ var it_default = {
|
|
|
8589
8604
|
"system.addon-updated.body": "{{list}}",
|
|
8590
8605
|
"system.server-updated.title": "Server aggiornato",
|
|
8591
8606
|
"system.server-updated.body": "{{packageName}} sul nodo {{nodeId}} è ora {{toVersion}} (era {{fromVersion}})",
|
|
8607
|
+
"system.server-updated.body.toOnly": "{{packageName}} sul nodo {{nodeId}} è ora {{toVersion}}",
|
|
8592
8608
|
"system.server-updated.body.plain": "{{packageName}} sul nodo {{nodeId}} è stato aggiornato",
|
|
8593
8609
|
"system.export-completed.title": "Export pronto",
|
|
8594
8610
|
"system.export-completed.body": "L'export del filmato è pronto",
|
|
@@ -10411,15 +10427,15 @@ function incomingFromUpdateAvailable(facts, data) {
|
|
|
10411
10427
|
const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== void 0));
|
|
10412
10428
|
const packageNames = unique(packages.map((pkg) => pkg.packageName));
|
|
10413
10429
|
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
10430
|
const head = packages[0];
|
|
10417
10431
|
const oneNode = nodeIds.length === 1 ? nodeIds[0] : void 0;
|
|
10432
|
+
const list = packages.map((pkg) => formatAvailableLine(pkg, oneNode !== void 0)).join("\n");
|
|
10433
|
+
const nodes = nodeIds.join(", ");
|
|
10418
10434
|
return systemSubject({
|
|
10419
10435
|
facts,
|
|
10420
10436
|
kind,
|
|
10421
10437
|
subject: addon ? `packages:addon:${signature}` : `packages:server:${signature}`,
|
|
10422
|
-
titleKey: `system.${kind}.title`,
|
|
10438
|
+
titleKey: addon && oneNode !== void 0 ? `system.${kind}.title.onNode` : `system.${kind}.title`,
|
|
10423
10439
|
bodyKey: !addon && oneNode !== void 0 ? `system.${kind}.body.onNode` : !addon && nodeIds.length > 1 ? `system.${kind}.body.onNodes` : `system.${kind}.body`,
|
|
10424
10440
|
textParams: {
|
|
10425
10441
|
list,
|
|
@@ -10478,12 +10494,12 @@ function incomingFromAddonUpdates(facts, updates) {
|
|
|
10478
10494
|
function incomingFromServerUpdated(facts, data) {
|
|
10479
10495
|
const kind = stringField(data, "kind");
|
|
10480
10496
|
const requestedBy = stringField(data, "requestedBy");
|
|
10481
|
-
if (!(kind === "framework-update" || kind === "framework-bulk-update" || requestedBy !== void 0 && requestedBy.startsWith("server-update:"))) return null;
|
|
10482
10497
|
const packageName = stringField(data, "packageName") ?? "@camstack/server";
|
|
10483
10498
|
const fromVersion = stringField(data, "fromVersion");
|
|
10484
10499
|
const toVersion = stringField(data, "toVersion") ?? versionFromRequestedBy(requestedBy);
|
|
10500
|
+
if (!(kind === "framework-update" || kind === "framework-bulk-update" || requestedBy !== void 0 && requestedBy.startsWith("server-update:") && toVersion !== void 0)) return null;
|
|
10485
10501
|
const nodeId = stringField(data, "nodeId") ?? "hub";
|
|
10486
|
-
const bodyKey =
|
|
10502
|
+
const bodyKey = toVersion === void 0 ? "system.server-updated.body.plain" : fromVersion === void 0 ? "system.server-updated.body.toOnly" : "system.server-updated.body";
|
|
10487
10503
|
return systemSubject({
|
|
10488
10504
|
facts,
|
|
10489
10505
|
kind: "server-updated",
|
|
@@ -10564,9 +10580,15 @@ function updatePackages(data) {
|
|
|
10564
10580
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10565
10581
|
}];
|
|
10566
10582
|
}
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10583
|
+
/**
|
|
10584
|
+
* One line of an availability list — the SAME shape the completed-update list
|
|
10585
|
+
* uses (`incomingFromAddonUpdates`), and deliberately word-free: the list is a
|
|
10586
|
+
* frozen param, so any English in it ("current") is delivered verbatim to an
|
|
10587
|
+
* Italian household. An arrow is not a language.
|
|
10588
|
+
*/
|
|
10589
|
+
function formatAvailableLine(pkg, nodeNamedInTitle) {
|
|
10590
|
+
const line = `${pkg.packageName} ${pkg.currentVersion} → ${pkg.latestVersion}`;
|
|
10591
|
+
return pkg.nodeId !== void 0 && !nodeNamedInTitle ? `${line} (${pkg.nodeId})` : line;
|
|
10570
10592
|
}
|
|
10571
10593
|
function versionFromRequestedBy(requestedBy) {
|
|
10572
10594
|
if (requestedBy === void 0) return void 0;
|
|
@@ -25184,7 +25206,16 @@ function clamp01$1(x) {
|
|
|
25184
25206
|
if (x > 1) return 1;
|
|
25185
25207
|
return x;
|
|
25186
25208
|
}
|
|
25187
|
-
/**
|
|
25209
|
+
/**
|
|
25210
|
+
* Rank a class name into [0,1] by how event-worthy it is. Case-insensitive.
|
|
25211
|
+
*
|
|
25212
|
+
* `audio` falls through to {@link CLASS_RANK_DEFAULT} on purpose — a sound is
|
|
25213
|
+
* not a subject in frame. Before D189 an audio episode carried the HEARD class
|
|
25214
|
+
* here, so `dog`/`cat`/`animal` landed in {@link ANIMAL_CLASSES} and a bark
|
|
25215
|
+
* ranked as if an animal had walked past, while `speech` (500 of the 576 audio
|
|
25216
|
+
* rows measured live) already ranked default. That boost was an accident of the
|
|
25217
|
+
* class collision, not a decision. Pinned by `key-event-scoring.spec.ts`.
|
|
25218
|
+
*/
|
|
25188
25219
|
function classRank(className) {
|
|
25189
25220
|
const c = className.toLowerCase();
|
|
25190
25221
|
if (PERSON_CLASSES.has(c)) return 1;
|
|
@@ -28811,19 +28842,29 @@ var BEST_FRAME_POLICY = {
|
|
|
28811
28842
|
sizeAware: true
|
|
28812
28843
|
};
|
|
28813
28844
|
/**
|
|
28814
|
-
* Best FACE (face-recognizer hold buffer) —
|
|
28815
|
-
*
|
|
28816
|
-
*
|
|
28817
|
-
*
|
|
28818
|
-
*
|
|
28819
|
-
*
|
|
28820
|
-
*
|
|
28845
|
+
* Best FACE (face-recognizer hold buffer) — SIZE-AWARE, no hysteresis, no rate
|
|
28846
|
+
* limit.
|
|
28847
|
+
*
|
|
28848
|
+
* No hysteresis / no gap, unchanged: accepting a new best only swaps an
|
|
28849
|
+
* already-decoded crop buffer in memory (cheap; no capture is triggered), and a
|
|
28850
|
+
* track may live well under the best-frame rate-limit window, so a gap would
|
|
28851
|
+
* silently discard the real peak for nothing.
|
|
28852
|
+
*
|
|
28853
|
+
* `sizeAware` since 2026-08-19. It used to be confidence-ONLY, and that was the
|
|
28854
|
+
* wrong ranking: SCRFD saturates its confidence on surveillance footage, so a
|
|
28855
|
+
* 12 px face and a 40 px face both answer ~0.9 and the track simply kept
|
|
28856
|
+
* whichever arrived first. The size that matters is the face's DETECTION-FRAME
|
|
28857
|
+
* short side, because that bounds the landmark precision the alignment warp
|
|
28858
|
+
* consumes — `face-size-score.ts` carries the measurement that set the
|
|
28859
|
+
* reference. Because `minGapMs` is 0 here, enabling this does NOT switch on the
|
|
28860
|
+
* decisive-improvement rate-limit bypass (there is no rate limit to bypass); it
|
|
28861
|
+
* only adds the size factor to the ranking.
|
|
28821
28862
|
*/
|
|
28822
28863
|
var BEST_FACE_POLICY = {
|
|
28823
28864
|
hysteresis: 0,
|
|
28824
28865
|
minGapMs: 0,
|
|
28825
28866
|
edgeAware: false,
|
|
28826
|
-
sizeAware:
|
|
28867
|
+
sizeAware: true
|
|
28827
28868
|
};
|
|
28828
28869
|
/**
|
|
28829
28870
|
* Best OBJECT (CLIP) embedding + its tight crop — confidence-only in-memory
|
|
@@ -32248,7 +32289,11 @@ var AudioDetectionSettingsSchema = require_dist.object({
|
|
|
32248
32289
|
* thresholds, is what bounds the worst case — a mis-tuned threshold can
|
|
32249
32290
|
* only ever cost the operator this many markers a day. */
|
|
32250
32291
|
audioMarkerMaxPerDay: require_dist.number().int().positive().default(12),
|
|
32251
|
-
/** Materialize one event + one synthetic track per classified episode.
|
|
32292
|
+
/** Materialize one event + one synthetic track per classified episode.
|
|
32293
|
+
* OFF switches off the GROUPING only — classified audio still persists an
|
|
32294
|
+
* event (class change, then one per 5 s heartbeat) and still reaches the
|
|
32295
|
+
* Notification Center; it just opens no synthetic track. A knob that names
|
|
32296
|
+
* the grouping must never silence the path (D62). */
|
|
32252
32297
|
audioEpisodeEnabled: require_dist.boolean().default(true),
|
|
32253
32298
|
/** Class must be heard continuously this long before the episode opens. */
|
|
32254
32299
|
audioEpisodeOpenHoldSec: require_dist.number().nonnegative().default(1.5),
|
|
@@ -34468,11 +34513,44 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
34468
34513
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
34469
34514
|
}
|
|
34470
34515
|
//#endregion
|
|
34516
|
+
//#region src/pipeline-analytics/pipeline/embedding-magnitude-gate.ts
|
|
34517
|
+
/**
|
|
34518
|
+
* `null` = accept. A verdict = refuse, carrying both numbers so the caller can
|
|
34519
|
+
* name them in a log.
|
|
34520
|
+
*
|
|
34521
|
+
* Accepts unconditionally when the bound is 0 (disabled) or when the magnitude
|
|
34522
|
+
* is absent / non-finite — an older runner that does not report it must keep
|
|
34523
|
+
* working exactly as before, never be silently gated to nothing. Pure.
|
|
34524
|
+
*/
|
|
34525
|
+
function embeddingMagnitudeVerdict(magnitude, bound) {
|
|
34526
|
+
if (!(bound > 0)) return null;
|
|
34527
|
+
if (magnitude === void 0 || !Number.isFinite(magnitude)) return null;
|
|
34528
|
+
if (magnitude <= bound) return null;
|
|
34529
|
+
return {
|
|
34530
|
+
magnitude,
|
|
34531
|
+
bound
|
|
34532
|
+
};
|
|
34533
|
+
}
|
|
34534
|
+
/**
|
|
34535
|
+
* Map a detection-frame face short side to the 0..1 `sizeScore` the
|
|
34536
|
+
* {@link ./best-detection-tracker.BestDetectionTracker} multiplies into its
|
|
34537
|
+
* effective score. Linear up to {@link FACE_SIZE_SCORE_REFERENCE_PX}, saturated
|
|
34538
|
+
* above it, 0 for a missing/degenerate measurement (so an unmeasurable face can
|
|
34539
|
+
* never outrank a measured one). Pure.
|
|
34540
|
+
*/
|
|
34541
|
+
function faceSizeScore(detectionFrameShortSidePx) {
|
|
34542
|
+
if (!Number.isFinite(detectionFrameShortSidePx) || detectionFrameShortSidePx <= 0) return 0;
|
|
34543
|
+
if (detectionFrameShortSidePx >= 32) return 1;
|
|
34544
|
+
return detectionFrameShortSidePx / 32;
|
|
34545
|
+
}
|
|
34546
|
+
//#endregion
|
|
34471
34547
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
34472
34548
|
/** Attribution of an identity resolved by the in-process ArcFace matcher. */
|
|
34473
34549
|
var FACE_RECOGNIZER_STEP_ID = "face-recognizer";
|
|
34474
34550
|
/** At most one "dropping imageless track" log per this interval, per recognizer. */
|
|
34475
34551
|
var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
|
|
34552
|
+
/** At most one 'embedding magnitude' sample line per device per this interval. */
|
|
34553
|
+
var FACE_MAGNITUDE_LOG_THROTTLE_MS = 6e4;
|
|
34476
34554
|
/**
|
|
34477
34555
|
* arcface model id stamped on a detail-plane face candidate when the gallery is
|
|
34478
34556
|
* empty (collect-only). The detail-subtree result carries no `embeddingModelId`
|
|
@@ -34487,6 +34565,8 @@ var FaceRecognizer = class {
|
|
|
34487
34565
|
names = /* @__PURE__ */ new Map();
|
|
34488
34566
|
aggregates = /* @__PURE__ */ new Map();
|
|
34489
34567
|
bestFace = /* @__PURE__ */ new Map();
|
|
34568
|
+
/** Per-device throttle clock for the magnitude sample line. */
|
|
34569
|
+
lastMagnitudeLogAt = /* @__PURE__ */ new Map();
|
|
34490
34570
|
/**
|
|
34491
34571
|
* Peak identity-MATCH cosine per track for the currently-assigned identity
|
|
34492
34572
|
* (NOT the detection confidence held in {@link bestFace}). Reset on an
|
|
@@ -34536,6 +34616,21 @@ var FaceRecognizer = class {
|
|
|
34536
34616
|
* input source moves from the per-frame plane to this per-track call.
|
|
34537
34617
|
*/
|
|
34538
34618
|
async ingestFaceDetail(input) {
|
|
34619
|
+
const verdict = embeddingMagnitudeVerdict(input.embeddingMagnitude, input.settings.maxEmbeddingMagnitude);
|
|
34620
|
+
if (verdict !== null) {
|
|
34621
|
+
this.deps.logger.info("face: dropping detail — raw embedding magnitude over bound", {
|
|
34622
|
+
tags: { deviceId: input.deviceId },
|
|
34623
|
+
meta: {
|
|
34624
|
+
trackId: input.trackId,
|
|
34625
|
+
magnitude: Number(verdict.magnitude.toFixed(3)),
|
|
34626
|
+
bound: verdict.bound,
|
|
34627
|
+
score: input.score,
|
|
34628
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: input.nativeFaceShortSidePx } : {}
|
|
34629
|
+
}
|
|
34630
|
+
});
|
|
34631
|
+
return;
|
|
34632
|
+
}
|
|
34633
|
+
this.logEmbeddingMagnitude(input);
|
|
34539
34634
|
const modelId = this.gallery[0]?.modelId ?? FALLBACK_FACE_MODEL_ID;
|
|
34540
34635
|
const candidate = {
|
|
34541
34636
|
trackId: input.trackId,
|
|
@@ -34562,6 +34657,30 @@ var FaceRecognizer = class {
|
|
|
34562
34657
|
...input.frameHandle !== void 0 ? { frameHandle: input.frameHandle } : {}
|
|
34563
34658
|
});
|
|
34564
34659
|
}
|
|
34660
|
+
/**
|
|
34661
|
+
* At most one magnitude line per device per
|
|
34662
|
+
* {@link FACE_MAGNITUDE_LOG_THROTTLE_MS}. This is the ONLY way the operator
|
|
34663
|
+
* can see where this cluster's usable and degenerate face populations sit, so
|
|
34664
|
+
* it must exist before `maxEmbeddingMagnitude` can honestly be turned on.
|
|
34665
|
+
*/
|
|
34666
|
+
logEmbeddingMagnitude(input) {
|
|
34667
|
+
const magnitude = input.embeddingMagnitude;
|
|
34668
|
+
if (magnitude === void 0 || !Number.isFinite(magnitude)) return;
|
|
34669
|
+
const now = Date.now();
|
|
34670
|
+
if (now - (this.lastMagnitudeLogAt.get(input.deviceId) ?? 0) < FACE_MAGNITUDE_LOG_THROTTLE_MS) return;
|
|
34671
|
+
this.lastMagnitudeLogAt.set(input.deviceId, now);
|
|
34672
|
+
this.deps.logger.info("face: embedding magnitude", {
|
|
34673
|
+
tags: { deviceId: input.deviceId },
|
|
34674
|
+
meta: {
|
|
34675
|
+
trackId: input.trackId,
|
|
34676
|
+
magnitude: Number(magnitude.toFixed(3)),
|
|
34677
|
+
bound: input.settings.maxEmbeddingMagnitude,
|
|
34678
|
+
score: input.score,
|
|
34679
|
+
detectionFrameShortSidePx: input.faceBbox !== void 0 ? Math.round(Math.min(input.faceBbox.w, input.faceBbox.h)) : 0,
|
|
34680
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: Math.round(input.nativeFaceShortSidePx) } : {}
|
|
34681
|
+
}
|
|
34682
|
+
});
|
|
34683
|
+
}
|
|
34565
34684
|
async processFrame(input) {
|
|
34566
34685
|
const { settings } = input;
|
|
34567
34686
|
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 +34742,8 @@ var FaceRecognizer = class {
|
|
|
34623
34742
|
}
|
|
34624
34743
|
const recognizedIdentityId = state.assignedIdentityId ?? void 0;
|
|
34625
34744
|
const held = this.bestFace.get(c.trackId);
|
|
34626
|
-
const
|
|
34745
|
+
const detectionFrameShortSidePx = c.faceBbox !== void 0 ? Math.min(c.faceBbox.w, c.faceBbox.h) : 0;
|
|
34746
|
+
const isNewBest = this.bestTracker.observe(c.trackId, c.confidence, input.timestamp, void 0, void 0, void 0, faceSizeScore(detectionFrameShortSidePx));
|
|
34627
34747
|
const needsCrop = held !== void 0 && held.crop === void 0;
|
|
34628
34748
|
if (isNewBest || needsCrop) {
|
|
34629
34749
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
@@ -34883,14 +35003,38 @@ var FaceSettingsSchema = require_dist.object({
|
|
|
34883
35003
|
* stripped from the per-device schema).
|
|
34884
35004
|
*/
|
|
34885
35005
|
enabled: require_dist.boolean().default(true),
|
|
34886
|
-
/**
|
|
34887
|
-
*
|
|
34888
|
-
*
|
|
34889
|
-
*
|
|
34890
|
-
|
|
34891
|
-
|
|
34892
|
-
*
|
|
34893
|
-
|
|
35006
|
+
/**
|
|
35007
|
+
* Cosine similarity (on L2-normalized arcface vectors) required to match.
|
|
35008
|
+
*
|
|
35009
|
+
* History: 0.55→0.62 on 2026-07-23, then back to 0.55 on 2026-08-18 once the
|
|
35010
|
+
* dead `assignUniquePerFrame` margin and the poisoned gallery were fixed —
|
|
35011
|
+
* with both repaired, genuine probes scored median 0.38 / p90 0.51, so 0.62
|
|
35012
|
+
* left almost nothing recognisable. That retune lived only as a RUNTIME
|
|
35013
|
+
* override until 2026-08-19; it is the schema default now, because a default
|
|
35014
|
+
* nobody runs is a decision nobody made.
|
|
35015
|
+
*
|
|
35016
|
+
* ⚠ NOT the end state. 0.55 is the least-bad value for the CURRENT crop
|
|
35017
|
+
* quality, where most of what reaches ArcFace is not a correctly aligned
|
|
35018
|
+
* face (measured 2026-08-19 over 60 live crops pulled from the hub: 20%
|
|
35019
|
+
* aligned, 18% marginal, 62% misaligned or containing no detectable face).
|
|
35020
|
+
* On correctly aligned crops the SAME models separate at a far lower
|
|
35021
|
+
* operating point — 300 LFW pairs gave genuine median 0.605 / impostor p90
|
|
35022
|
+
* 0.175, best accuracy 98.3% at threshold 0.28.
|
|
35023
|
+
*
|
|
35024
|
+
* Recalibration plan, in this order:
|
|
35025
|
+
* 1. land the landmark-precision gate (the alignment fix),
|
|
35026
|
+
* 2. re-enrol the gallery from crops produced AFTER it,
|
|
35027
|
+
* 3. re-measure genuine/impostor over a week of post-fix traffic,
|
|
35028
|
+
* 4. expect to lower this toward ~0.30, and `margin` with it.
|
|
35029
|
+
* Lowering it BEFORE step 1 would be actively harmful: a misaligned crop
|
|
35030
|
+
* scores ~0.9 against everything, so the degenerate population sits ABOVE
|
|
35031
|
+
* any threshold you could pick — the knob cannot reach it.
|
|
35032
|
+
*/
|
|
35033
|
+
similarityThreshold: require_dist.number().min(0).max(1).default(.55),
|
|
35034
|
+
/** Reject ambiguous matches: require best − secondBest ≥ margin. 0.10→0.15
|
|
35035
|
+
* (2026-07-23), then →0.12 (2026-08-18) alongside the threshold retune —
|
|
35036
|
+
* same story and the same recalibration plan as {@link similarityThreshold}. */
|
|
35037
|
+
margin: require_dist.number().min(0).max(1).default(.12),
|
|
34894
35038
|
/** Minimum face-detection confidence for a face to be considered. */
|
|
34895
35039
|
minFaceConfidence: require_dist.number().min(0).max(1).default(.5),
|
|
34896
35040
|
/**
|
|
@@ -34934,6 +35078,33 @@ var FaceSettingsSchema = require_dist.object({
|
|
|
34934
35078
|
* 1 (0 confirmations would assign on a single noisy frame — nonsensical;
|
|
34935
35079
|
* such a value falls back to the default). */
|
|
34936
35080
|
confirmFrames: require_dist.number().int().min(1).default(3),
|
|
35081
|
+
/**
|
|
35082
|
+
* Upper bound on the RAW ArcFace feature magnitude a face may have and still
|
|
35083
|
+
* be embedded into the buffer / matched / enrolled. `0` (the default) DISABLES
|
|
35084
|
+
* the gate.
|
|
35085
|
+
*
|
|
35086
|
+
* This exists because a degenerate crop does not score LOW, it scores HIGH:
|
|
35087
|
+
* measured on this cluster 2026-08-18/19, 28.8% of all pairs of DIFFERENT
|
|
35088
|
+
* faces exceeded cosine 0.90 and the worst 40% of the face population had
|
|
35089
|
+
* collapsed onto effectively one vector (mean pairwise cosine 0.976). No
|
|
35090
|
+
* similarity threshold can reach that — it sits above every threshold — so the
|
|
35091
|
+
* only defence is to refuse the embedding before it is stored or compared.
|
|
35092
|
+
*
|
|
35093
|
+
* Direction: for this model the DEGENERATE crops carry the LARGER norm (the
|
|
35094
|
+
* inverse of the MagFace convention), hence a maximum. Over 140 LFW faces
|
|
35095
|
+
* through this exact model and preprocessing, usable crops sat at ≈4.4 (p95
|
|
35096
|
+
* 5.25) regardless of size, grey level or blur, while information-free crops
|
|
35097
|
+
* jumped to 10–16; a bound of 6.0 kept 97.6% of usable crops and rejected
|
|
35098
|
+
* 93.2% of degenerate ones.
|
|
35099
|
+
*
|
|
35100
|
+
* Default 0 ON PURPOSE. Those numbers are LFW's, not this cluster's, and the
|
|
35101
|
+
* magnitude has never been observable here — `result-assembler` discarded it
|
|
35102
|
+
* one line after computing it. The field and its logging ship first; set a
|
|
35103
|
+
* real bound once a week of live `face: embedding magnitude` lines says where
|
|
35104
|
+
* this cluster's populations actually sit. Enabling a gate against a number
|
|
35105
|
+
* nobody has measured in production is how recognition silently stops.
|
|
35106
|
+
*/
|
|
35107
|
+
maxEmbeddingMagnitude: require_dist.number().min(0).default(0),
|
|
34937
35108
|
/** Max buffered faces kept per device. */
|
|
34938
35109
|
bufferMaxPerDevice: require_dist.number().int().min(0).default(50)
|
|
34939
35110
|
});
|
|
@@ -34952,6 +35123,7 @@ function resolveFaceSettings(raw) {
|
|
|
34952
35123
|
recognitionMinFacePx: pick("recognitionMinFacePx"),
|
|
34953
35124
|
suggestionMinCosine: pick("suggestionMinCosine"),
|
|
34954
35125
|
minIdentitySamples: pick("minIdentitySamples"),
|
|
35126
|
+
maxEmbeddingMagnitude: pick("maxEmbeddingMagnitude"),
|
|
34955
35127
|
confirmFrames: pick("confirmFrames"),
|
|
34956
35128
|
bufferMaxPerDevice: pick("bufferMaxPerDevice")
|
|
34957
35129
|
};
|
|
@@ -39423,6 +39595,147 @@ async function applyTrackFlags(deps, input) {
|
|
|
39423
39595
|
};
|
|
39424
39596
|
}
|
|
39425
39597
|
//#endregion
|
|
39598
|
+
//#region src/pipeline-analytics/pipeline/plate-vote.ts
|
|
39599
|
+
/**
|
|
39600
|
+
* Per-character VOTE over a track's plate reads.
|
|
39601
|
+
*
|
|
39602
|
+
* A track produces up to `plate-ocr`'s `cadence.maxPerTrack` reads of the SAME
|
|
39603
|
+
* physical plate, from different frames. The recognizer used to keep the single
|
|
39604
|
+
* read with the highest OCR score (`holdBest`, argmax) — and OCR score does not
|
|
39605
|
+
* correlate with correctness. In the 2026-08 audit the top-scoring read of a
|
|
39606
|
+
* track was routinely the wrong one while two lower-scoring reads agreed on the
|
|
39607
|
+
* right glyph: score ranks how confident the recognizer was about the pixels it
|
|
39608
|
+
* saw, not how much of the plate those pixels were.
|
|
39609
|
+
*
|
|
39610
|
+
* So the reads vote. Each read is normalized (upper, alnum only), aligned to a
|
|
39611
|
+
* reference read, and contributes its OCR score to whichever glyph it saw at
|
|
39612
|
+
* each reference position; the winner per position is the output. Two reads
|
|
39613
|
+
* that agree beat one confident read that disagrees, which is the whole point.
|
|
39614
|
+
*
|
|
39615
|
+
* ALIGNMENT — reads differ in length (4–9 glyphs live: a dropped leading
|
|
39616
|
+
* district letter, a hallucinated trailing glyph). A positional vote on raw
|
|
39617
|
+
* indices would let one missing character shift every later column and destroy
|
|
39618
|
+
* the agreement it was supposed to measure. Each read is therefore aligned to
|
|
39619
|
+
* the reference by Levenshtein traceback: a match or substitution votes at the
|
|
39620
|
+
* reference position it aligned to, an insertion votes nowhere, and a deletion
|
|
39621
|
+
* simply abstains at that position.
|
|
39622
|
+
*
|
|
39623
|
+
* REFERENCE — the length carrying the most SCORE MASS wins (a hallucinated
|
|
39624
|
+
* 9-glyph read does not get to set the output length just by being longest);
|
|
39625
|
+
* ties inside that length go to the highest-scoring read. With ≤5 reads this is
|
|
39626
|
+
* the honest cheap heuristic: there is no third source of truth to consult.
|
|
39627
|
+
*/
|
|
39628
|
+
/**
|
|
39629
|
+
* Align `candidate` to `reference` by Levenshtein traceback. Returns one entry
|
|
39630
|
+
* per REFERENCE position: the candidate glyph that aligned there, or `null`
|
|
39631
|
+
* when the candidate had nothing at that position (a deletion). Candidate
|
|
39632
|
+
* insertions are dropped — they have no reference column to vote in.
|
|
39633
|
+
*/
|
|
39634
|
+
function alignToReference(reference, candidate) {
|
|
39635
|
+
const n = reference.length;
|
|
39636
|
+
const m = candidate.length;
|
|
39637
|
+
const dist = [];
|
|
39638
|
+
for (let i = 0; i <= n; i++) {
|
|
39639
|
+
const row = new Array(m + 1).fill(0);
|
|
39640
|
+
row[0] = i;
|
|
39641
|
+
dist.push(row);
|
|
39642
|
+
}
|
|
39643
|
+
const first = dist[0];
|
|
39644
|
+
if (first === void 0) return [];
|
|
39645
|
+
for (let j = 0; j <= m; j++) first[j] = j;
|
|
39646
|
+
for (let i = 1; i <= n; i++) {
|
|
39647
|
+
const prev = dist[i - 1];
|
|
39648
|
+
const cur = dist[i];
|
|
39649
|
+
if (prev === void 0 || cur === void 0) continue;
|
|
39650
|
+
for (let j = 1; j <= m; j++) {
|
|
39651
|
+
const cost = reference[i - 1] === candidate[j - 1] ? 0 : 1;
|
|
39652
|
+
cur[j] = Math.min((prev[j] ?? 0) + 1, (cur[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
39653
|
+
}
|
|
39654
|
+
}
|
|
39655
|
+
const aligned = new Array(n).fill(null);
|
|
39656
|
+
let i = n;
|
|
39657
|
+
let j = m;
|
|
39658
|
+
while (i > 0 && j > 0) {
|
|
39659
|
+
const cur = dist[i];
|
|
39660
|
+
const prev = dist[i - 1];
|
|
39661
|
+
if (cur === void 0 || prev === void 0) break;
|
|
39662
|
+
const cost = reference[i - 1] === candidate[j - 1] ? 0 : 1;
|
|
39663
|
+
if (cur[j] === (prev[j - 1] ?? 0) + cost) {
|
|
39664
|
+
aligned[i - 1] = candidate[j - 1] ?? null;
|
|
39665
|
+
i--;
|
|
39666
|
+
j--;
|
|
39667
|
+
} else if (cur[j] === (prev[j] ?? 0) + 1) i--;
|
|
39668
|
+
else j--;
|
|
39669
|
+
}
|
|
39670
|
+
return aligned;
|
|
39671
|
+
}
|
|
39672
|
+
/** The read whose LENGTH carries the most score mass; ties → highest score. */
|
|
39673
|
+
function pickReference(reads) {
|
|
39674
|
+
const massByLength = /* @__PURE__ */ new Map();
|
|
39675
|
+
for (const r of reads) massByLength.set(r.text.length, (massByLength.get(r.text.length) ?? 0) + r.score);
|
|
39676
|
+
let bestLength = 0;
|
|
39677
|
+
let bestMass = -1;
|
|
39678
|
+
for (const [length, mass] of massByLength) if (mass > bestMass || mass === bestMass && length < bestLength) {
|
|
39679
|
+
bestMass = mass;
|
|
39680
|
+
bestLength = length;
|
|
39681
|
+
}
|
|
39682
|
+
let reference = "";
|
|
39683
|
+
let referenceScore = -1;
|
|
39684
|
+
for (const r of reads) if (r.text.length === bestLength && r.score > referenceScore) {
|
|
39685
|
+
referenceScore = r.score;
|
|
39686
|
+
reference = r.text;
|
|
39687
|
+
}
|
|
39688
|
+
return reference;
|
|
39689
|
+
}
|
|
39690
|
+
/**
|
|
39691
|
+
* Confidence-weighted per-character vote over a track's plate reads. Returns
|
|
39692
|
+
* the normalized winning text (`''` when there is nothing to vote on).
|
|
39693
|
+
*/
|
|
39694
|
+
function votePlateText(reads) {
|
|
39695
|
+
const normalized = [];
|
|
39696
|
+
for (const r of reads) {
|
|
39697
|
+
const text = normalizePlate(r.text);
|
|
39698
|
+
if (text.length > 0) normalized.push({
|
|
39699
|
+
text,
|
|
39700
|
+
score: r.score
|
|
39701
|
+
});
|
|
39702
|
+
}
|
|
39703
|
+
if (normalized.length === 0) return "";
|
|
39704
|
+
const only = normalized[0];
|
|
39705
|
+
if (normalized.length === 1 && only !== void 0) return only.text;
|
|
39706
|
+
const reference = pickReference(normalized);
|
|
39707
|
+
if (reference.length === 0) return "";
|
|
39708
|
+
const columns = [];
|
|
39709
|
+
for (let i = 0; i < reference.length; i++) columns.push(/* @__PURE__ */ new Map());
|
|
39710
|
+
for (const r of normalized) {
|
|
39711
|
+
const aligned = r.text === reference ? [...r.text] : alignToReference(reference, r.text);
|
|
39712
|
+
for (let i = 0; i < reference.length; i++) {
|
|
39713
|
+
const glyph = aligned[i];
|
|
39714
|
+
if (glyph === null || glyph === void 0) continue;
|
|
39715
|
+
const column = columns[i];
|
|
39716
|
+
if (column === void 0) continue;
|
|
39717
|
+
column.set(glyph, (column.get(glyph) ?? 0) + r.score);
|
|
39718
|
+
}
|
|
39719
|
+
}
|
|
39720
|
+
let out = "";
|
|
39721
|
+
for (let i = 0; i < reference.length; i++) {
|
|
39722
|
+
const column = columns[i];
|
|
39723
|
+
const referenceGlyph = reference[i] ?? "";
|
|
39724
|
+
if (column === void 0 || column.size === 0) {
|
|
39725
|
+
out += referenceGlyph;
|
|
39726
|
+
continue;
|
|
39727
|
+
}
|
|
39728
|
+
let bestGlyph = referenceGlyph;
|
|
39729
|
+
let bestMass = column.get(referenceGlyph) ?? -1;
|
|
39730
|
+
for (const [glyph, mass] of column) if (mass > bestMass) {
|
|
39731
|
+
bestMass = mass;
|
|
39732
|
+
bestGlyph = glyph;
|
|
39733
|
+
}
|
|
39734
|
+
out += bestGlyph;
|
|
39735
|
+
}
|
|
39736
|
+
return out;
|
|
39737
|
+
}
|
|
39738
|
+
//#endregion
|
|
39426
39739
|
//#region src/pipeline-analytics/pipeline/plate-matcher.ts
|
|
39427
39740
|
/**
|
|
39428
39741
|
* plate-matcher — name a plate read against the vehicle gallery by text vicinity.
|
|
@@ -39471,11 +39784,11 @@ function matchPlateText(probe, gallery, opts) {
|
|
|
39471
39784
|
//#endregion
|
|
39472
39785
|
//#region src/pipeline-analytics/plate-recognizer.ts
|
|
39473
39786
|
/**
|
|
39474
|
-
* PlateRecognizer —
|
|
39475
|
-
*
|
|
39476
|
-
*
|
|
39477
|
-
*
|
|
39478
|
-
*
|
|
39787
|
+
* PlateRecognizer — collects EVERY license-plate OCR read a vehicle track
|
|
39788
|
+
* produces and stores their per-character VOTE (`plate-vote.ts`) at close, plus
|
|
39789
|
+
* the max-score read's crop for the gallery tile. Unlike faces there is NO
|
|
39790
|
+
* gallery matching: the OCR text IS the label and already rides on the event via
|
|
39791
|
+
* the detection pipeline. Mirrors FaceRecognizer's hold→onTrackEnd lifecycle.
|
|
39479
39792
|
*/
|
|
39480
39793
|
var PlateRecognizer = class {
|
|
39481
39794
|
deps;
|
|
@@ -39538,7 +39851,7 @@ var PlateRecognizer = class {
|
|
|
39538
39851
|
const minConfidence = input.minConfidence ?? 0;
|
|
39539
39852
|
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
39853
|
if (candidates.length === 0) return;
|
|
39541
|
-
for (const c of candidates) await this.
|
|
39854
|
+
for (const c of candidates) await this.observeRead({
|
|
39542
39855
|
deviceId: input.deviceId,
|
|
39543
39856
|
trackId: c.trackId,
|
|
39544
39857
|
text: c.plateText,
|
|
@@ -39561,15 +39874,50 @@ var PlateRecognizer = class {
|
|
|
39561
39874
|
* gallery stays empty (0 plateCrop) — the observed live gap.
|
|
39562
39875
|
*/
|
|
39563
39876
|
async observePlateRead(input) {
|
|
39564
|
-
|
|
39565
|
-
|
|
39566
|
-
|
|
39877
|
+
const minConfidence = input.minConfidence ?? 0;
|
|
39878
|
+
const reason = !isPlausiblePlateRead(input.text, input.score) ? "implausible" : input.score < minConfidence ? "below-min-confidence" : null;
|
|
39879
|
+
if (reason !== null) {
|
|
39880
|
+
this.deps.logger.info("plate: read discarded", {
|
|
39881
|
+
tags: {
|
|
39882
|
+
deviceId: input.deviceId,
|
|
39883
|
+
trackId: input.trackId
|
|
39884
|
+
},
|
|
39885
|
+
meta: {
|
|
39886
|
+
text: input.text,
|
|
39887
|
+
score: input.score,
|
|
39888
|
+
minConfidence,
|
|
39889
|
+
reason
|
|
39890
|
+
}
|
|
39891
|
+
});
|
|
39892
|
+
return;
|
|
39893
|
+
}
|
|
39894
|
+
await this.observeRead(input);
|
|
39567
39895
|
}
|
|
39568
|
-
/**
|
|
39569
|
-
*
|
|
39570
|
-
|
|
39896
|
+
/**
|
|
39897
|
+
* Record one plate read for a track (shared by the per-frame + detail-plane
|
|
39898
|
+
* paths).
|
|
39899
|
+
*
|
|
39900
|
+
* EVERY read is kept for the close-time vote — a track's reads disagree, and
|
|
39901
|
+
* the disagreement is the signal. The crop + geometry still track the
|
|
39902
|
+
* MAX-SCORE read: the vote decides the text, but the picture the operator
|
|
39903
|
+
* reviews should be the sharpest frame the track produced.
|
|
39904
|
+
*
|
|
39905
|
+
* The read list is capped at {@link MAX_VOTING_READS} in ARRIVAL order, not
|
|
39906
|
+
* by score: evicting the lowest-scoring read would re-introduce the very
|
|
39907
|
+
* ranking this vote exists to stop trusting.
|
|
39908
|
+
*/
|
|
39909
|
+
async observeRead(input) {
|
|
39571
39910
|
const held = this.bestPlate.get(input.trackId);
|
|
39572
|
-
|
|
39911
|
+
const vote = {
|
|
39912
|
+
text: input.text,
|
|
39913
|
+
score: input.score
|
|
39914
|
+
};
|
|
39915
|
+
if (held !== void 0 && input.score <= held.score) {
|
|
39916
|
+
if (held.reads.length < 5) held.reads.push(vote);
|
|
39917
|
+
return;
|
|
39918
|
+
}
|
|
39919
|
+
const reads = held?.reads ?? [];
|
|
39920
|
+
if (reads.length < 5) reads.push(vote);
|
|
39573
39921
|
let crop;
|
|
39574
39922
|
if (input.frameHandle !== void 0) try {
|
|
39575
39923
|
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId, input.deviceId) ?? void 0;
|
|
@@ -39585,6 +39933,7 @@ var PlateRecognizer = class {
|
|
|
39585
39933
|
this.bestPlate.set(input.trackId, {
|
|
39586
39934
|
text: input.text,
|
|
39587
39935
|
score: input.score,
|
|
39936
|
+
reads,
|
|
39588
39937
|
bbox: input.bbox,
|
|
39589
39938
|
timestamp: input.timestamp,
|
|
39590
39939
|
frameWidth: input.frameWidth,
|
|
@@ -39661,14 +40010,16 @@ var PlateRecognizer = class {
|
|
|
39661
40010
|
}
|
|
39662
40011
|
});
|
|
39663
40012
|
}
|
|
39664
|
-
const
|
|
40013
|
+
const voted = votePlateText(held.reads);
|
|
40014
|
+
const text = voted.length > 0 && isPlausiblePlateRead(voted, held.score) ? voted : normalizePlate(held.text);
|
|
40015
|
+
const match = this.matchVehicle(text, held.score);
|
|
39665
40016
|
try {
|
|
39666
40017
|
await this.deps.plateStore.insert({
|
|
39667
40018
|
id: plateId,
|
|
39668
40019
|
deviceId,
|
|
39669
40020
|
trackId,
|
|
39670
40021
|
timestamp: held.timestamp,
|
|
39671
|
-
text
|
|
40022
|
+
text,
|
|
39672
40023
|
score: held.score,
|
|
39673
40024
|
...mediaKey !== void 0 ? { mediaKey } : {},
|
|
39674
40025
|
corrected: false,
|
|
@@ -39689,7 +40040,10 @@ var PlateRecognizer = class {
|
|
|
39689
40040
|
},
|
|
39690
40041
|
meta: {
|
|
39691
40042
|
plateId,
|
|
39692
|
-
text
|
|
40043
|
+
text,
|
|
40044
|
+
bestRead: normalizePlate(held.text),
|
|
40045
|
+
reads: held.reads.map((r) => `${normalizePlate(r.text)}@${r.score.toFixed(2)}`),
|
|
40046
|
+
votedFromReads: held.reads.length,
|
|
39693
40047
|
score: held.score,
|
|
39694
40048
|
hasCrop: mediaKey !== void 0,
|
|
39695
40049
|
recognizedVehicleId: match?.vehicleId ?? null
|
|
@@ -40207,6 +40561,24 @@ function buildRecordedStillPorts(input) {
|
|
|
40207
40561
|
var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
40208
40562
|
var MIGRATION_MARKER_KEY = "label-tier-4g";
|
|
40209
40563
|
/**
|
|
40564
|
+
* The owner declares its marker collection — like every other store in this
|
|
40565
|
+
* addon. KV shape (a single `data` column: `get`/`set` route the value as a
|
|
40566
|
+
* JSON blob; the engine adds the implicit `id` primary key). This declaration
|
|
40567
|
+
* never existed, so the engine refused every marker write, the catch below
|
|
40568
|
+
* warned, and every boot re-scanned the full analytics store — ~36k rows per
|
|
40569
|
+
* boot from the day the migration shipped.
|
|
40570
|
+
*/
|
|
40571
|
+
async function declareMigrationMarkers(store) {
|
|
40572
|
+
await store.declareCollection.mutate({
|
|
40573
|
+
collection: MIGRATION_MARKER_COLLECTION,
|
|
40574
|
+
columns: [{
|
|
40575
|
+
name: "data",
|
|
40576
|
+
type: "TEXT",
|
|
40577
|
+
notNull: true
|
|
40578
|
+
}]
|
|
40579
|
+
});
|
|
40580
|
+
}
|
|
40581
|
+
/**
|
|
40210
40582
|
* The decision for ONE row, exposed so the rule is testable without a store.
|
|
40211
40583
|
*
|
|
40212
40584
|
* `null` = leave the row alone.
|
|
@@ -43387,6 +43759,47 @@ function canRollbackSnapshot(mark, currentLastSnapshotAt) {
|
|
|
43387
43759
|
return mark.priorRollbacks < 3;
|
|
43388
43760
|
}
|
|
43389
43761
|
//#endregion
|
|
43762
|
+
//#region src/pipeline-analytics/store/audio-track-class.ts
|
|
43763
|
+
/** The taxonomy macro every audio-sourced track carries as its class. */
|
|
43764
|
+
var AUDIO_TRACK_CLASS = "audio";
|
|
43765
|
+
/**
|
|
43766
|
+
* True when this row predates the contract: audio-sourced, but carrying a
|
|
43767
|
+
* naked audio macro as its class.
|
|
43768
|
+
*
|
|
43769
|
+
* Deliberately NOT "className is in AUDIO_MACRO_LABELS" — an id added to the
|
|
43770
|
+
* vocabulary after a row was written would then read as modern forever. The
|
|
43771
|
+
* question is the simple one: is this an audio row that does not say `audio`?
|
|
43772
|
+
*/
|
|
43773
|
+
function isLegacyAudioTrackClass(track) {
|
|
43774
|
+
return track.source === "audio" && track.className !== "audio";
|
|
43775
|
+
}
|
|
43776
|
+
/**
|
|
43777
|
+
* Bring a hydrated row up to the contract. Returns the SAME reference when
|
|
43778
|
+
* there is nothing to do, so the hot path (every non-audio track, and every
|
|
43779
|
+
* audio track written after this shipped) allocates nothing.
|
|
43780
|
+
*
|
|
43781
|
+
* The naked class is preserved as an `audioLabels` entry when — and only when
|
|
43782
|
+
* — the row carries none. On the 576 legacy rows measured on the live hub that
|
|
43783
|
+
* branch is unreachable (all of them have labels); it exists so the rewrite
|
|
43784
|
+
* can never be the step that DESTROYS the only copy of what was heard.
|
|
43785
|
+
*/
|
|
43786
|
+
function normalizeAudioTrackClass(track) {
|
|
43787
|
+
if (!isLegacyAudioTrackClass(track)) return track;
|
|
43788
|
+
const heard = {
|
|
43789
|
+
label: track.className,
|
|
43790
|
+
peakScore: 0,
|
|
43791
|
+
count: 1,
|
|
43792
|
+
firstAt: track.firstSeen,
|
|
43793
|
+
lastAt: track.lastSeen
|
|
43794
|
+
};
|
|
43795
|
+
const audioLabels = track.audioLabels !== void 0 && track.audioLabels.length > 0 ? track.audioLabels : [heard];
|
|
43796
|
+
return {
|
|
43797
|
+
...track,
|
|
43798
|
+
className: AUDIO_TRACK_CLASS,
|
|
43799
|
+
audioLabels
|
|
43800
|
+
};
|
|
43801
|
+
}
|
|
43802
|
+
//#endregion
|
|
43390
43803
|
//#region src/pipeline-analytics/store/recent-cursor.ts
|
|
43391
43804
|
function encodeRecentCursor(cursor) {
|
|
43392
43805
|
return Buffer.from(JSON.stringify({
|
|
@@ -45034,7 +45447,7 @@ var TrackStore = class {
|
|
|
45034
45447
|
maxX: envMaxX,
|
|
45035
45448
|
maxY: envMaxY
|
|
45036
45449
|
} : null;
|
|
45037
|
-
return {
|
|
45450
|
+
return normalizeAudioTrackClass({
|
|
45038
45451
|
trackId: id,
|
|
45039
45452
|
deviceId: Number(data["deviceId"]),
|
|
45040
45453
|
className: String(data["className"]),
|
|
@@ -45065,7 +45478,7 @@ var TrackStore = class {
|
|
|
45065
45478
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
45066
45479
|
observations,
|
|
45067
45480
|
...envelope !== null ? { envelope } : {}
|
|
45068
|
-
};
|
|
45481
|
+
});
|
|
45069
45482
|
}
|
|
45070
45483
|
};
|
|
45071
45484
|
//#endregion
|
|
@@ -47398,7 +47811,7 @@ var AudioMarkerProjector = class {
|
|
|
47398
47811
|
cameraId: deviceId,
|
|
47399
47812
|
sourceDeviceId: deviceId,
|
|
47400
47813
|
source: "audio",
|
|
47401
|
-
kind:
|
|
47814
|
+
kind: AUDIO_TRACK_CLASS,
|
|
47402
47815
|
timestamp: timestampMs,
|
|
47403
47816
|
...audioLabels !== void 0 ? { audioLabels } : {}
|
|
47404
47817
|
});
|
|
@@ -48606,6 +49019,23 @@ async function projectSensorMarkers(deps, data, timestamp) {
|
|
|
48606
49019
|
* {@link isSpatialTrack}.
|
|
48607
49020
|
*/
|
|
48608
49021
|
var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
49022
|
+
/**
|
|
49023
|
+
* What the debounce treats as "the same event happening again".
|
|
49024
|
+
*
|
|
49025
|
+
* `(camera, kind)` — plus, for an audio row, the class HEARD. Since D189 every
|
|
49026
|
+
* audio row carries `kind: 'audio'` and puts the class on `audioLabels`, so
|
|
49027
|
+
* keying on `kind` alone would collapse this to one bucket per camera. The
|
|
49028
|
+
* episode hysteresis cools down PER CLASS, so a bark three seconds after speech
|
|
49029
|
+
* is a legitimate second episode — and a per-camera bucket would drop it.
|
|
49030
|
+
*
|
|
49031
|
+
* Only the FIRST label is used: a materialize carries the one class the episode
|
|
49032
|
+
* opened on. Joining all of them would make `[speech]` and `[speech, dog]`
|
|
49033
|
+
* different buckets, which is the opposite of what a debounce is for.
|
|
49034
|
+
*/
|
|
49035
|
+
function debounceKey(input) {
|
|
49036
|
+
const heard = input.audioLabels?.[0]?.label;
|
|
49037
|
+
return heard === void 0 ? `${input.cameraId}:${input.kind}` : `${input.cameraId}:${input.kind}:${heard}`;
|
|
49038
|
+
}
|
|
48609
49039
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
48610
49040
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
48611
49041
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
@@ -48648,7 +49078,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
48648
49078
|
snapshotTimeoutMs;
|
|
48649
49079
|
makeId;
|
|
48650
49080
|
now;
|
|
48651
|
-
/** Last snapshot time per
|
|
49081
|
+
/** Last snapshot time per {@link debounceKey}. */
|
|
48652
49082
|
lastAt = /* @__PURE__ */ new Map();
|
|
48653
49083
|
constructor(deps) {
|
|
48654
49084
|
this.deps = deps;
|
|
@@ -48687,7 +49117,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
48687
49117
|
* still lands a track (with no media) — it never blocks the durable record.
|
|
48688
49118
|
*/
|
|
48689
49119
|
async materialize(input) {
|
|
48690
|
-
const key =
|
|
49120
|
+
const key = debounceKey(input);
|
|
48691
49121
|
const last = this.lastAt.get(key);
|
|
48692
49122
|
if (last !== void 0 && this.now() - last < this.debounceMs) return null;
|
|
48693
49123
|
this.lastAt.set(key, this.now());
|
|
@@ -50042,7 +50472,7 @@ function buildGlobalSettingsSchema() {
|
|
|
50042
50472
|
type: "boolean",
|
|
50043
50473
|
key: "audioEpisodeEnabled",
|
|
50044
50474
|
label: "Create audio episodes",
|
|
50045
|
-
description: "
|
|
50475
|
+
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
50476
|
default: AUDIO_DETECTION_DEFAULTS.audioEpisodeEnabled
|
|
50047
50477
|
},
|
|
50048
50478
|
{
|
|
@@ -50168,6 +50598,15 @@ function buildGlobalSettingsSchema() {
|
|
|
50168
50598
|
step: 1,
|
|
50169
50599
|
default: FACE_DEFAULTS.confirmFrames
|
|
50170
50600
|
},
|
|
50601
|
+
{
|
|
50602
|
+
type: "number",
|
|
50603
|
+
key: "maxEmbeddingMagnitude",
|
|
50604
|
+
label: "Max embedding magnitude (0 = off)",
|
|
50605
|
+
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.",
|
|
50606
|
+
min: 0,
|
|
50607
|
+
step: .5,
|
|
50608
|
+
default: FACE_DEFAULTS.maxEmbeddingMagnitude
|
|
50609
|
+
},
|
|
50171
50610
|
{
|
|
50172
50611
|
type: "number",
|
|
50173
50612
|
key: "bufferMaxPerDevice",
|
|
@@ -50245,6 +50684,125 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
50245
50684
|
movedFraction
|
|
50246
50685
|
};
|
|
50247
50686
|
}
|
|
50687
|
+
/** Where the completion marker lives — the collection `runLabelTierMigration`
|
|
50688
|
+
* already established for exactly this. */
|
|
50689
|
+
var AUDIO_CLASS_MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
50690
|
+
var AUDIO_CLASS_MIGRATION_MARKER_KEY = "audio-track-class-d189";
|
|
50691
|
+
/**
|
|
50692
|
+
* The decision for ONE stored row, exposed so the rule is testable without a
|
|
50693
|
+
* store. `null` = leave the row alone.
|
|
50694
|
+
*
|
|
50695
|
+
* Reads the RAW row rather than a hydrated `Track` on purpose: this is the
|
|
50696
|
+
* column the pass rewrites, and hydrating through `rowToTrack` would apply the
|
|
50697
|
+
* read-side normalization and hide the very rows it is looking for.
|
|
50698
|
+
*/
|
|
50699
|
+
function decideAudioClassRowMigration(data) {
|
|
50700
|
+
if (data["source"] !== "audio") return null;
|
|
50701
|
+
const className = data["className"];
|
|
50702
|
+
if (typeof className !== "string" || className.length === 0) return null;
|
|
50703
|
+
if (className === "audio") return null;
|
|
50704
|
+
const stored = data["audioLabels"];
|
|
50705
|
+
if (Array.isArray(stored) && stored.length > 0) return { className: AUDIO_TRACK_CLASS };
|
|
50706
|
+
const firstSeen = typeof data["firstSeen"] === "number" ? data["firstSeen"] : 0;
|
|
50707
|
+
return {
|
|
50708
|
+
className: AUDIO_TRACK_CLASS,
|
|
50709
|
+
audioLabels: [{
|
|
50710
|
+
label: className,
|
|
50711
|
+
peakScore: 0,
|
|
50712
|
+
count: 1,
|
|
50713
|
+
firstAt: firstSeen,
|
|
50714
|
+
lastAt: typeof data["lastSeen"] === "number" ? data["lastSeen"] : firstSeen
|
|
50715
|
+
}]
|
|
50716
|
+
};
|
|
50717
|
+
}
|
|
50718
|
+
/**
|
|
50719
|
+
* Run the pass over the tracks collection, once.
|
|
50720
|
+
*
|
|
50721
|
+
* The marker short-circuits a completed migration; it is written only when the
|
|
50722
|
+
* pass did NOT truncate, so an interrupted run resumes on the next boot instead
|
|
50723
|
+
* of declaring itself done.
|
|
50724
|
+
*/
|
|
50725
|
+
async function runAudioTrackClassMigration(deps, collection) {
|
|
50726
|
+
try {
|
|
50727
|
+
const marker = await deps.store.get.query({
|
|
50728
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50729
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY
|
|
50730
|
+
});
|
|
50731
|
+
if (marker !== null && marker !== void 0) return null;
|
|
50732
|
+
} catch {}
|
|
50733
|
+
let scanned = 0;
|
|
50734
|
+
let rewritten = 0;
|
|
50735
|
+
let labelsRecovered = 0;
|
|
50736
|
+
let skipped = 0;
|
|
50737
|
+
let offset = 0;
|
|
50738
|
+
let truncated = false;
|
|
50739
|
+
for (;;) {
|
|
50740
|
+
if (scanned >= 2e5) {
|
|
50741
|
+
truncated = true;
|
|
50742
|
+
break;
|
|
50743
|
+
}
|
|
50744
|
+
const rows = await deps.store.query.query({
|
|
50745
|
+
collection,
|
|
50746
|
+
filter: {
|
|
50747
|
+
limit: 500,
|
|
50748
|
+
offset
|
|
50749
|
+
}
|
|
50750
|
+
});
|
|
50751
|
+
if (rows.length === 0) break;
|
|
50752
|
+
for (const row of rows) {
|
|
50753
|
+
scanned++;
|
|
50754
|
+
const patch = decideAudioClassRowMigration(row.data);
|
|
50755
|
+
if (patch === null) {
|
|
50756
|
+
skipped++;
|
|
50757
|
+
continue;
|
|
50758
|
+
}
|
|
50759
|
+
const deviceId = typeof row.data["deviceId"] === "number" ? row.data["deviceId"] : void 0;
|
|
50760
|
+
try {
|
|
50761
|
+
await deps.store.update.mutate({
|
|
50762
|
+
collection,
|
|
50763
|
+
id: row.id,
|
|
50764
|
+
data: patch
|
|
50765
|
+
});
|
|
50766
|
+
rewritten++;
|
|
50767
|
+
if (patch["audioLabels"] !== void 0) labelsRecovered++;
|
|
50768
|
+
} catch (err) {
|
|
50769
|
+
deps.logger.warn("audio track class migration row failed", {
|
|
50770
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
50771
|
+
meta: {
|
|
50772
|
+
collection,
|
|
50773
|
+
id: row.id,
|
|
50774
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50775
|
+
}
|
|
50776
|
+
});
|
|
50777
|
+
skipped++;
|
|
50778
|
+
}
|
|
50779
|
+
}
|
|
50780
|
+
if (rows.length < 500) break;
|
|
50781
|
+
offset += rows.length;
|
|
50782
|
+
}
|
|
50783
|
+
if (!truncated) try {
|
|
50784
|
+
await deps.store.set.mutate({
|
|
50785
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50786
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY,
|
|
50787
|
+
value: {
|
|
50788
|
+
completedAt: deps.now(),
|
|
50789
|
+
rewritten
|
|
50790
|
+
}
|
|
50791
|
+
});
|
|
50792
|
+
} catch (err) {
|
|
50793
|
+
deps.logger.warn("audio track class migration marker failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50794
|
+
}
|
|
50795
|
+
const stats = {
|
|
50796
|
+
collection,
|
|
50797
|
+
scanned,
|
|
50798
|
+
rewritten,
|
|
50799
|
+
labelsRecovered,
|
|
50800
|
+
skipped,
|
|
50801
|
+
truncated
|
|
50802
|
+
};
|
|
50803
|
+
deps.logger.info("audio track class migration", { meta: { ...stats } });
|
|
50804
|
+
return stats;
|
|
50805
|
+
}
|
|
50248
50806
|
//#endregion
|
|
50249
50807
|
//#region src/pipeline-analytics/store/object-embedding-store.ts
|
|
50250
50808
|
/**
|
|
@@ -51548,6 +52106,16 @@ var OVERLAY_SYNTHESIS_WARN_THROTTLE_MS = 6e4;
|
|
|
51548
52106
|
var AUDIO_EPISODE_VISUAL_QUIET_MS = 2e3;
|
|
51549
52107
|
/** Persist lastSeen on an open synthetic audio track at most this often. */
|
|
51550
52108
|
var AUDIO_EPISODE_PERSIST_MS = 2e3;
|
|
52109
|
+
/**
|
|
52110
|
+
* Coalescing window for classified audio inserts when episodes are OFF.
|
|
52111
|
+
*
|
|
52112
|
+
* With `audioEpisodeEnabled: false` there is no hysteresis to collapse a sound
|
|
52113
|
+
* into one row, but the classifier still fires at ~30 Hz — so the pre-episode
|
|
52114
|
+
* rule applies again: while the SAME class persists, one row per heartbeat;
|
|
52115
|
+
* a class change inserts immediately. The knob turns off the grouping, never
|
|
52116
|
+
* the persistence (D62).
|
|
52117
|
+
*/
|
|
52118
|
+
var AUDIO_EVENT_HEARTBEAT_MS = 5e3;
|
|
51551
52119
|
var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
51552
52120
|
/**
|
|
51553
52121
|
* Stored media kinds that carry NO drawn bounding box, in fallback preference
|
|
@@ -52653,7 +53221,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
52653
53221
|
AnalyticsLts.declare(api.settingsStore),
|
|
52654
53222
|
SceneStore.declare(api.settingsStore),
|
|
52655
53223
|
NotificationCenter.declare(api.settingsStore),
|
|
52656
|
-
ViewerSettingsSnapshotStore.declare(api.settingsStore)
|
|
53224
|
+
ViewerSettingsSnapshotStore.declare(api.settingsStore),
|
|
53225
|
+
declareMigrationMarkers(api.settingsStore)
|
|
52657
53226
|
]);
|
|
52658
53227
|
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
52659
53228
|
try {
|
|
@@ -52671,6 +53240,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
52671
53240
|
}, [TRACKS_COLLECTION, OBJECT_EVENTS_COLLECTION]).catch((err) => {
|
|
52672
53241
|
this.ctx.logger.warn("label tier migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
52673
53242
|
});
|
|
53243
|
+
runAudioTrackClassMigration({
|
|
53244
|
+
store: api.settingsStore,
|
|
53245
|
+
logger: this.ctx.logger,
|
|
53246
|
+
now: () => Date.now()
|
|
53247
|
+
}, TRACKS_COLLECTION).catch((err) => {
|
|
53248
|
+
this.ctx.logger.warn("audio track class migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
53249
|
+
});
|
|
52674
53250
|
}
|
|
52675
53251
|
/** Event-media storage. By default it uses `ctx.kernel.storage`, rooted at
|
|
52676
53252
|
* the node data dir (appData) → heavy thumbnail/track-frame churn hammers
|
|
@@ -55403,6 +55979,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55403
55979
|
frameHeight: frame.frameHeight,
|
|
55404
55980
|
score: detail.score,
|
|
55405
55981
|
embedding: decodeEmbeddingBase64(detail.embedding),
|
|
55982
|
+
...detail.embeddingMagnitude !== void 0 ? { embeddingMagnitude: detail.embeddingMagnitude } : {},
|
|
55406
55983
|
parentBbox: { ...frame.bbox },
|
|
55407
55984
|
...detail.bbox !== void 0 ? { faceBbox: { ...detail.bbox } } : {},
|
|
55408
55985
|
...detail.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: detail.nativeFaceShortSidePx } : {},
|
|
@@ -55870,7 +56447,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55870
56447
|
return;
|
|
55871
56448
|
}
|
|
55872
56449
|
}
|
|
55873
|
-
|
|
56450
|
+
/**
|
|
56451
|
+
* The ONE writer for a classified audio observation: the durable row, the
|
|
56452
|
+
* Notification Center hook fired in the same persist moment (D-2), the
|
|
56453
|
+
* track-level `audioLabels` stamp, and the telemetry event.
|
|
56454
|
+
*
|
|
56455
|
+
* Shared by both routes on purpose. `audioEpisodeEnabled` selects WHEN this
|
|
56456
|
+
* runs (once per episode, or once per heartbeat), never WHETHER — a knob
|
|
56457
|
+
* that names the grouping must not be able to silence the classified path
|
|
56458
|
+
* (D62). This body was inlined in `openAudioEpisode` and the knob-off camera
|
|
56459
|
+
* therefore persisted nothing at all.
|
|
56460
|
+
*/
|
|
56461
|
+
async persistClassifiedAudio(input) {
|
|
55874
56462
|
const ev = {
|
|
55875
56463
|
id: (0, node_crypto.randomUUID)(),
|
|
55876
56464
|
deviceId: input.deviceId,
|
|
@@ -55879,18 +56467,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55879
56467
|
rms: input.rms,
|
|
55880
56468
|
dbfs: input.dbfs,
|
|
55881
56469
|
classification: {
|
|
55882
|
-
className:
|
|
56470
|
+
className: input.className,
|
|
55883
56471
|
...input.originalClass !== void 0 ? { originalClass: input.originalClass } : {},
|
|
55884
56472
|
score: input.score
|
|
55885
56473
|
}
|
|
55886
56474
|
};
|
|
55887
56475
|
this.lastAudioInsertByDevice.set(input.deviceId, {
|
|
55888
|
-
className:
|
|
56476
|
+
className: input.className,
|
|
55889
56477
|
atMs: input.timestamp
|
|
55890
56478
|
});
|
|
55891
56479
|
await this.eventStore.insertAudio(ev);
|
|
55892
56480
|
this.notificationCenter?.onAudioEventPersisted(ev);
|
|
55893
|
-
this.trackStore?.addAudioLabelEpisode(input.deviceId,
|
|
56481
|
+
this.trackStore?.addAudioLabelEpisode(input.deviceId, input.className, input.score, input.timestamp);
|
|
55894
56482
|
this.ctx.eventBus.emit({
|
|
55895
56483
|
id: `pa-${ev.id}`,
|
|
55896
56484
|
timestamp: new Date(ev.timestamp),
|
|
@@ -55907,6 +56495,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55907
56495
|
timestamp: ev.timestamp
|
|
55908
56496
|
}
|
|
55909
56497
|
});
|
|
56498
|
+
}
|
|
56499
|
+
/**
|
|
56500
|
+
* Classified audio with episodes OFF: persist on class change or heartbeat,
|
|
56501
|
+
* and open NO synthetic track. This is the whole of what the knob switches —
|
|
56502
|
+
* the timeline track and the episode coalescing — and it is what the form
|
|
56503
|
+
* promises.
|
|
56504
|
+
*/
|
|
56505
|
+
async persistClassifiedAudioUngrouped(input) {
|
|
56506
|
+
const last = this.lastAudioInsertByDevice.get(input.deviceId);
|
|
56507
|
+
const sameClass = last?.className === input.className;
|
|
56508
|
+
const heartbeatDue = last === void 0 || input.timestamp - last.atMs >= AUDIO_EVENT_HEARTBEAT_MS;
|
|
56509
|
+
if (sameClass && !heartbeatDue) return;
|
|
56510
|
+
await this.persistClassifiedAudio(input);
|
|
56511
|
+
}
|
|
56512
|
+
async openAudioEpisode(input, ref) {
|
|
56513
|
+
await this.persistClassifiedAudio({
|
|
56514
|
+
...input,
|
|
56515
|
+
className: ref.className
|
|
56516
|
+
});
|
|
55910
56517
|
if (this.hasLiveVisualTrack(input.deviceId, input.timestamp)) return;
|
|
55911
56518
|
const materializer = this.syntheticTracks;
|
|
55912
56519
|
if (materializer === null) return;
|
|
@@ -55915,7 +56522,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55915
56522
|
cameraId: input.deviceId,
|
|
55916
56523
|
sourceDeviceId: input.deviceId,
|
|
55917
56524
|
source: "audio",
|
|
55918
|
-
kind:
|
|
56525
|
+
kind: AUDIO_TRACK_CLASS,
|
|
55919
56526
|
timestamp: input.timestamp,
|
|
55920
56527
|
audioLabels: [{
|
|
55921
56528
|
label: ref.className,
|
|
@@ -56018,19 +56625,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
56018
56625
|
});
|
|
56019
56626
|
return;
|
|
56020
56627
|
}
|
|
56021
|
-
|
|
56628
|
+
const classified = {
|
|
56022
56629
|
deviceId,
|
|
56023
56630
|
timestamp,
|
|
56024
56631
|
className: route.className,
|
|
56025
56632
|
score: topClassification.score,
|
|
56026
|
-
originalClass: topClassification.debug
|
|
56633
|
+
...topClassification.debug?.originalClass !== void 0 ? { originalClass: topClassification.debug.originalClass } : {},
|
|
56027
56634
|
rms: level?.rms ?? 0,
|
|
56028
|
-
dbfs: level?.dbfs ?? 0
|
|
56635
|
+
dbfs: level?.dbfs ?? 0
|
|
56636
|
+
};
|
|
56637
|
+
if (!settings.audioEpisodeEnabled) {
|
|
56638
|
+
await this.persistClassifiedAudioUngrouped(classified);
|
|
56639
|
+
return;
|
|
56640
|
+
}
|
|
56641
|
+
await this.driveAudioEpisode({
|
|
56642
|
+
...classified,
|
|
56029
56643
|
knobs: this.audioEpisodeKnobs(settings)
|
|
56030
56644
|
});
|
|
56031
56645
|
return;
|
|
56032
56646
|
}
|
|
56033
|
-
await this.driveAudioEpisode({
|
|
56647
|
+
if (settings.audioEpisodeEnabled) await this.driveAudioEpisode({
|
|
56034
56648
|
deviceId,
|
|
56035
56649
|
timestamp,
|
|
56036
56650
|
className: null,
|